Spaces:
Running
Conversation trees (#223) (#807)
Browse files* work on branching
* branching
* work on input field
* wip
* wip
* helper stuff
* pass tests
* add type guards and clean up type requirements
* fixed shared conv type
* more helpers stuff
* clean up code
* addChildren helper
* addSibling
* add test for addSibling & refacto addChildren tests
* backend work pt. 1
* backend done
* add children property to messages for easier frontend rendering
* fix message id type rail
* front-end work on simple linear conversation
* fix title generation
* convert conversations on post
* server side retry works with branching
* clean up buttons
* fix retry feature backend
* Send new messages in any subtree
* make edit previous prompts feature work
* fix padding
* revert unneeded changes
* bring back pending message
* fix front-end streaming
* fix initial message
* Revert "fix initial message"
This reverts commit 6257fe8789c52a20e7506aa453efda55c16e95bb.
* Fix bug subtree state refresh
* fix continue feature on shared conversations
* fix websearch updates
* Fix first message streaming
* fix fornt-end websearch updates
* fix loading icon
* fix bottom padding
* move children nav to below the message
* Show current message in continue & retry
* children nav styling
* you can now edit the first message
* bottom padding on assistant message
* fix test
* lint
* use <form>
* tree navigation
* misc
* mobile: hide download link
* forgot to implem continue feature lol
* fix continue feature on llama 70b
* fix edit mode
* disable submit button & nav when loading
* fix bug when interrupting
* hide arrows in edit mode
* forgot to reset edit mode when submitting retry
* reset editing when switching conversations
---------
Co-authored-by: Victor Mustar <victor.mustar@gmail.com>
- .env.template +1 -5
- src/lib/buildPrompt.ts +19 -82
- src/lib/components/chat/ChatMessage.svelte +196 -45
- src/lib/components/chat/ChatMessages.svelte +0 -106
- src/lib/components/chat/ChatWindow.svelte +115 -40
- src/lib/server/database.ts +6 -0
- src/lib/server/endpoints/aws/endpointAws.ts +4 -4
- src/lib/server/endpoints/endpoints.ts +4 -7
- src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +4 -4
- src/lib/server/endpoints/ollama/endpointOllama.ts +4 -4
- src/lib/server/endpoints/openai/endpointOai.ts +16 -43
- src/lib/server/endpoints/tgi/endpointTgi.ts +5 -16
- src/lib/server/generateFromDefaultEndpoint.ts +1 -1
- src/lib/server/preprocessMessages.ts +57 -0
- src/lib/server/websearch/runWebSearch.ts +2 -5
- src/lib/stores/convTree.ts +25 -0
- src/lib/types/Conversation.ts +1 -0
- src/lib/types/Message.ts +7 -1
- src/lib/types/SharedConversation.ts +14 -14
- src/lib/utils/tree/addChildren.spec.ts +102 -0
- src/lib/utils/tree/addChildren.ts +52 -0
- src/lib/utils/tree/addSibling.spec.ts +80 -0
- src/lib/utils/tree/addSibling.ts +45 -0
- src/lib/utils/tree/buildSubtree.spec.ts +110 -0
- src/lib/utils/tree/buildSubtree.ts +28 -0
- src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -0
- src/lib/utils/tree/convertLegacyConversation.ts +35 -0
- src/lib/utils/tree/isMessageId.spec.ts +14 -0
- src/lib/utils/tree/isMessageId.ts +5 -0
- src/lib/utils/tree/treeHelpers.spec.ts +164 -0
- src/routes/conversation/+server.ts +15 -1
- src/routes/conversation/[id]/+page.server.ts +10 -6
- src/routes/conversation/[id]/+page.svelte +118 -67
- src/routes/conversation/[id]/+server.ts +154 -112
- src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +4 -3
- src/routes/conversation/[id]/share/+server.ts +3 -2
|
@@ -33,10 +33,6 @@ MODELS=`[
|
|
| 33 |
"name": "meta-llama/Llama-2-70b-chat-hf",
|
| 34 |
"description": "The latest and biggest model from Meta, fine-tuned for chat.",
|
| 35 |
"websiteUrl": "https://ai.meta.com/llama/",
|
| 36 |
-
"userMessageToken": "",
|
| 37 |
-
"userMessageEndToken": " [/INST] ",
|
| 38 |
-
"assistantMessageToken": "",
|
| 39 |
-
"assistantMessageEndToken": " </s><s>[INST] ",
|
| 40 |
"preprompt": " ",
|
| 41 |
"chatPromptTemplate" : "<s>[INST] <<SYS>>\n{{preprompt}}\n<</SYS>>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} </s><s>[INST] {{/ifAssistant}}{{/each}}",
|
| 42 |
"promptExamples": [
|
|
@@ -58,7 +54,7 @@ MODELS=`[
|
|
| 58 |
"top_k": 50,
|
| 59 |
"truncate": 3072,
|
| 60 |
"max_new_tokens": 1024,
|
| 61 |
-
"stop" : ["</s>", "
|
| 62 |
}
|
| 63 |
},
|
| 64 |
{
|
|
|
|
| 33 |
"name": "meta-llama/Llama-2-70b-chat-hf",
|
| 34 |
"description": "The latest and biggest model from Meta, fine-tuned for chat.",
|
| 35 |
"websiteUrl": "https://ai.meta.com/llama/",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
"preprompt": " ",
|
| 37 |
"chatPromptTemplate" : "<s>[INST] <<SYS>>\n{{preprompt}}\n<</SYS>>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} </s><s>[INST] {{/ifAssistant}}{{/each}}",
|
| 38 |
"promptExamples": [
|
|
|
|
| 54 |
"top_k": 50,
|
| 55 |
"truncate": 3072,
|
| 56 |
"max_new_tokens": 1024,
|
| 57 |
+
"stop" : ["</s>", "</s><s>[INST]"]
|
| 58 |
}
|
| 59 |
},
|
| 60 |
{
|
|
@@ -1,94 +1,31 @@
|
|
|
|
|
| 1 |
import type { BackendModel } from "./server/models";
|
| 2 |
-
import type { Message } from "./types/Message";
|
| 3 |
-
import { format } from "date-fns";
|
| 4 |
-
import type { WebSearch } from "./types/WebSearch";
|
| 5 |
-
import { downloadFile } from "./server/files/downloadFile";
|
| 6 |
-
import type { Conversation } from "./types/Conversation";
|
| 7 |
|
| 8 |
-
|
| 9 |
-
messages: Pick<Message, "from" | "content" | "files">[];
|
| 10 |
-
id?: Conversation["_id"];
|
| 11 |
model: BackendModel;
|
| 12 |
-
|
| 13 |
-
webSearch?: WebSearch;
|
| 14 |
-
preprompt?: string;
|
| 15 |
-
files?: File[];
|
| 16 |
-
continue?: boolean;
|
| 17 |
-
}
|
| 18 |
|
| 19 |
export async function buildPrompt({
|
| 20 |
messages,
|
| 21 |
model,
|
| 22 |
-
webSearch,
|
| 23 |
preprompt,
|
| 24 |
-
|
| 25 |
}: buildPromptOptions): Promise<string> {
|
| 26 |
-
let
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
const currentDate = format(new Date(), "MMMM d, yyyy");
|
| 42 |
-
|
| 43 |
-
// update the last user message directly (that way if the last message is an assistant partial answer, we keep the beginning of that answer)
|
| 44 |
-
modifiedMessages[lastUsrMsgIndex] = {
|
| 45 |
-
from: "user",
|
| 46 |
-
content: `I searched the web using the query: ${webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
| 47 |
-
=====================
|
| 48 |
-
${webSearch.context}
|
| 49 |
-
=====================
|
| 50 |
-
${previousQuestions}
|
| 51 |
-
Answer the question: ${messages[lastUsrMsgIndex].content}`,
|
| 52 |
-
};
|
| 53 |
-
}
|
| 54 |
-
// section to handle potential files input
|
| 55 |
-
if (model.multimodal) {
|
| 56 |
-
modifiedMessages = await Promise.all(
|
| 57 |
-
modifiedMessages.map(async (el) => {
|
| 58 |
-
let content = el.content;
|
| 59 |
-
|
| 60 |
-
if (el.from === "user") {
|
| 61 |
-
if (el?.files && el.files.length > 0 && id) {
|
| 62 |
-
const markdowns = await Promise.all(
|
| 63 |
-
el.files.map(async (hash) => {
|
| 64 |
-
try {
|
| 65 |
-
const { content: image, mime } = await downloadFile(hash, id);
|
| 66 |
-
const b64 = image.toString("base64");
|
| 67 |
-
return `})`;
|
| 68 |
-
} catch (e) {
|
| 69 |
-
console.error(e);
|
| 70 |
-
}
|
| 71 |
-
})
|
| 72 |
-
);
|
| 73 |
-
content += markdowns.join("\n ");
|
| 74 |
-
} else {
|
| 75 |
-
// if no image, append an empty white image
|
| 76 |
-
content +=
|
| 77 |
-
"\n";
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
return { ...el, content };
|
| 82 |
-
})
|
| 83 |
-
);
|
| 84 |
}
|
| 85 |
|
| 86 |
-
return
|
| 87 |
-
model
|
| 88 |
-
.chatPromptRender({ messages: modifiedMessages, preprompt })
|
| 89 |
-
// Not super precise, but it's truncated in the model's backend anyway
|
| 90 |
-
.split(" ")
|
| 91 |
-
.slice(-(model.parameters?.truncate ?? 0))
|
| 92 |
-
.join(" ")
|
| 93 |
-
);
|
| 94 |
}
|
|
|
|
| 1 |
+
import type { EndpointParameters } from "./server/endpoints/endpoints";
|
| 2 |
import type { BackendModel } from "./server/models";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
type buildPromptOptions = Pick<EndpointParameters, "messages" | "preprompt" | "continueMessage"> & {
|
|
|
|
|
|
|
| 5 |
model: BackendModel;
|
| 6 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export async function buildPrompt({
|
| 9 |
messages,
|
| 10 |
model,
|
|
|
|
| 11 |
preprompt,
|
| 12 |
+
continueMessage,
|
| 13 |
}: buildPromptOptions): Promise<string> {
|
| 14 |
+
let prompt = model
|
| 15 |
+
.chatPromptRender({ messages, preprompt })
|
| 16 |
+
// Not super precise, but it's truncated in the model's backend anyway
|
| 17 |
+
.split(" ")
|
| 18 |
+
.slice(-(model.parameters?.truncate ?? 0))
|
| 19 |
+
.join(" ");
|
| 20 |
+
|
| 21 |
+
if (continueMessage && model.parameters?.stop) {
|
| 22 |
+
prompt = model.parameters.stop.reduce((acc: string, curr: string) => {
|
| 23 |
+
if (acc.endsWith(curr)) {
|
| 24 |
+
return acc.slice(0, acc.length - curr.length);
|
| 25 |
+
}
|
| 26 |
+
return acc;
|
| 27 |
+
}, prompt.trimEnd());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
+
return prompt;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
import { marked } from "marked";
|
| 3 |
import markedKatex from "marked-katex-extension";
|
| 4 |
import type { Message } from "$lib/types/Message";
|
| 5 |
-
import { afterUpdate, createEventDispatcher } from "svelte";
|
| 6 |
import { deepestChild } from "$lib/utils/deepestChild";
|
| 7 |
import { page } from "$app/stores";
|
| 8 |
|
|
@@ -13,6 +13,9 @@
|
|
| 13 |
import CarbonDownload from "~icons/carbon/download";
|
| 14 |
import CarbonThumbsUp from "~icons/carbon/thumbs-up";
|
| 15 |
import CarbonThumbsDown from "~icons/carbon/thumbs-down";
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
| 18 |
import type { Model } from "$lib/types/Model";
|
|
@@ -20,6 +23,7 @@
|
|
| 20 |
import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
|
| 21 |
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
| 22 |
import { base } from "$app/paths";
|
|
|
|
| 23 |
|
| 24 |
function sanitizeMd(md: string) {
|
| 25 |
let ret = md
|
|
@@ -45,16 +49,17 @@
|
|
| 45 |
}
|
| 46 |
|
| 47 |
export let model: Model;
|
| 48 |
-
export let
|
|
|
|
| 49 |
export let loading = false;
|
| 50 |
export let isAuthor = true;
|
| 51 |
export let readOnly = false;
|
| 52 |
export let isTapped = false;
|
| 53 |
|
| 54 |
-
|
| 55 |
|
| 56 |
const dispatch = createEventDispatcher<{
|
| 57 |
-
retry: { content
|
| 58 |
vote: { score: Message["score"]; id: Message["id"] };
|
| 59 |
}>();
|
| 60 |
|
|
@@ -63,6 +68,8 @@
|
|
| 63 |
let pendingTimeout: ReturnType<typeof setTimeout>;
|
| 64 |
let isCopied = false;
|
| 65 |
|
|
|
|
|
|
|
| 66 |
const renderer = new marked.Renderer();
|
| 67 |
// For code blocks with simple backticks
|
| 68 |
renderer.codespan = (code) => {
|
|
@@ -91,12 +98,15 @@
|
|
| 91 |
|
| 92 |
$: tokens = marked.lexer(sanitizeMd(message.content));
|
| 93 |
|
|
|
|
|
|
|
|
|
|
| 94 |
afterUpdate(() => {
|
| 95 |
loadingEl?.$destroy();
|
| 96 |
clearTimeout(pendingTimeout);
|
| 97 |
|
| 98 |
// Add loading animation to the last message if update takes more than 600ms
|
| 99 |
-
if (loading) {
|
| 100 |
pendingTimeout = setTimeout(() => {
|
| 101 |
if (contentEl) {
|
| 102 |
loadingEl = new IconLoading({
|
|
@@ -108,11 +118,14 @@
|
|
| 108 |
}
|
| 109 |
});
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
$: searchUpdates = ((
|
| 114 |
-
|
| 115 |
-
: message.updates?.filter(({ type }) => type === "webSearch")) ?? []) as WebSearchUpdate[];
|
| 116 |
|
| 117 |
$: downloadLink =
|
| 118 |
message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
|
|
@@ -131,11 +144,40 @@
|
|
| 131 |
isCopied = false;
|
| 132 |
}, 1000);
|
| 133 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
</script>
|
| 135 |
|
| 136 |
{#if message.from === "assistant"}
|
| 137 |
<div
|
| 138 |
-
class="group relative -mb-
|
| 139 |
role="presentation"
|
| 140 |
on:click={() => (isTapped = !isTapped)}
|
| 141 |
on:keydown={() => (isTapped = !isTapped)}
|
|
@@ -162,9 +204,6 @@
|
|
| 162 |
webSearchMessages={searchUpdates}
|
| 163 |
/>
|
| 164 |
{/if}
|
| 165 |
-
{#if !message.content && (webSearchIsDone || (webSearchMessages && webSearchMessages.length === 0))}
|
| 166 |
-
<IconLoading />
|
| 167 |
-
{/if}
|
| 168 |
|
| 169 |
<div
|
| 170 |
class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
|
@@ -179,6 +218,7 @@
|
|
| 179 |
{/if}
|
| 180 |
{/each}
|
| 181 |
</div>
|
|
|
|
| 182 |
<!-- Web Search sources -->
|
| 183 |
{#if webSearchSources?.length}
|
| 184 |
<div class="mt-4 flex flex-wrap items-center gap-x-2 gap-y-1.5 text-sm">
|
|
@@ -202,7 +242,7 @@
|
|
| 202 |
</div>
|
| 203 |
{#if isAuthor && !loading && message.content}
|
| 204 |
<div
|
| 205 |
-
class="absolute bottom-1 right-0 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
|
| 206 |
{message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
|
| 207 |
{isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
|
| 208 |
"
|
|
@@ -230,6 +270,14 @@
|
|
| 230 |
>
|
| 231 |
<CarbonThumbsDown class="h-[1.14em] w-[1.14em]" />
|
| 232 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
<CopyToClipBoardBtn
|
| 234 |
on:click={() => {
|
| 235 |
isCopied = true;
|
|
@@ -240,10 +288,16 @@
|
|
| 240 |
</div>
|
| 241 |
{/if}
|
| 242 |
</div>
|
|
|
|
| 243 |
{/if}
|
| 244 |
{#if message.from === "user"}
|
| 245 |
-
<div
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
{#if message.files && message.files.length > 0}
|
| 248 |
<div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
|
| 249 |
{#each message.files as file}
|
|
@@ -266,36 +320,133 @@
|
|
| 266 |
</div>
|
| 267 |
{/if}
|
| 268 |
|
| 269 |
-
<div
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
</div>
|
| 274 |
-
|
| 275 |
-
<div class="absolute right-0 top-3.5 flex gap-2 lg:-right-2">
|
| 276 |
-
{#if downloadLink}
|
| 277 |
-
<a
|
| 278 |
-
class="rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
| 279 |
-
title="Download prompt and parameters"
|
| 280 |
-
type="button"
|
| 281 |
-
target="_blank"
|
| 282 |
-
href={downloadLink}
|
| 283 |
-
>
|
| 284 |
-
<CarbonDownload />
|
| 285 |
-
</a>
|
| 286 |
-
{/if}
|
| 287 |
-
{#if !readOnly}
|
| 288 |
-
<button
|
| 289 |
-
class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden lg:-right-2 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
| 290 |
-
title="Retry"
|
| 291 |
-
type="button"
|
| 292 |
-
on:click={() => dispatch("retry", { content: message.content, id: message.id })}
|
| 293 |
-
>
|
| 294 |
-
<CarbonRotate360 />
|
| 295 |
-
</button>
|
| 296 |
-
{/if}
|
| 297 |
-
</div>
|
| 298 |
-
{/if}
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import { marked } from "marked";
|
| 3 |
import markedKatex from "marked-katex-extension";
|
| 4 |
import type { Message } from "$lib/types/Message";
|
| 5 |
+
import { afterUpdate, createEventDispatcher, tick } from "svelte";
|
| 6 |
import { deepestChild } from "$lib/utils/deepestChild";
|
| 7 |
import { page } from "$app/stores";
|
| 8 |
|
|
|
|
| 13 |
import CarbonDownload from "~icons/carbon/download";
|
| 14 |
import CarbonThumbsUp from "~icons/carbon/thumbs-up";
|
| 15 |
import CarbonThumbsDown from "~icons/carbon/thumbs-down";
|
| 16 |
+
import CarbonPen from "~icons/carbon/pen";
|
| 17 |
+
import CarbonChevronLeft from "~icons/carbon/chevron-left";
|
| 18 |
+
import CarbonChevronRight from "~icons/carbon/chevron-right";
|
| 19 |
|
| 20 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
| 21 |
import type { Model } from "$lib/types/Model";
|
|
|
|
| 23 |
import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
|
| 24 |
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
| 25 |
import { base } from "$app/paths";
|
| 26 |
+
import { useConvTreeStore } from "$lib/stores/convTree";
|
| 27 |
|
| 28 |
function sanitizeMd(md: string) {
|
| 29 |
let ret = md
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
export let model: Model;
|
| 52 |
+
export let id: Message["id"];
|
| 53 |
+
export let messages: Message[];
|
| 54 |
export let loading = false;
|
| 55 |
export let isAuthor = true;
|
| 56 |
export let readOnly = false;
|
| 57 |
export let isTapped = false;
|
| 58 |
|
| 59 |
+
$: message = messages.find((m) => m.id === id) ?? ({} as Message);
|
| 60 |
|
| 61 |
const dispatch = createEventDispatcher<{
|
| 62 |
+
retry: { content?: string; id: Message["id"] };
|
| 63 |
vote: { score: Message["score"]; id: Message["id"] };
|
| 64 |
}>();
|
| 65 |
|
|
|
|
| 68 |
let pendingTimeout: ReturnType<typeof setTimeout>;
|
| 69 |
let isCopied = false;
|
| 70 |
|
| 71 |
+
let initialized = false;
|
| 72 |
+
|
| 73 |
const renderer = new marked.Renderer();
|
| 74 |
// For code blocks with simple backticks
|
| 75 |
renderer.codespan = (code) => {
|
|
|
|
| 98 |
|
| 99 |
$: tokens = marked.lexer(sanitizeMd(message.content));
|
| 100 |
|
| 101 |
+
$: emptyLoad =
|
| 102 |
+
!message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0));
|
| 103 |
+
|
| 104 |
afterUpdate(() => {
|
| 105 |
loadingEl?.$destroy();
|
| 106 |
clearTimeout(pendingTimeout);
|
| 107 |
|
| 108 |
// Add loading animation to the last message if update takes more than 600ms
|
| 109 |
+
if ((loading && isLast) || emptyLoad) {
|
| 110 |
pendingTimeout = setTimeout(() => {
|
| 111 |
if (contentEl) {
|
| 112 |
loadingEl = new IconLoading({
|
|
|
|
| 118 |
}
|
| 119 |
});
|
| 120 |
|
| 121 |
+
function handleKeyDown(e: KeyboardEvent) {
|
| 122 |
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
| 123 |
+
editFormEl.requestSubmit();
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
|
| 127 |
+
$: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ??
|
| 128 |
+
[]) as WebSearchUpdate[];
|
|
|
|
| 129 |
|
| 130 |
$: downloadLink =
|
| 131 |
message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
|
|
|
|
| 144 |
isCopied = false;
|
| 145 |
}, 1000);
|
| 146 |
}
|
| 147 |
+
|
| 148 |
+
$: editMode = $convTreeStore.editing === message.id;
|
| 149 |
+
let editContentEl: HTMLTextAreaElement;
|
| 150 |
+
let editFormEl: HTMLFormElement;
|
| 151 |
+
|
| 152 |
+
$: if (editMode) {
|
| 153 |
+
tick();
|
| 154 |
+
if (editContentEl) {
|
| 155 |
+
editContentEl.value = message.content;
|
| 156 |
+
editContentEl?.focus();
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
$: isLast = (message && message.children?.length === 0) ?? false;
|
| 161 |
+
|
| 162 |
+
$: childrenToRender = 0;
|
| 163 |
+
$: nChildren = message?.children?.length ?? 0;
|
| 164 |
+
|
| 165 |
+
$: {
|
| 166 |
+
if (initialized) {
|
| 167 |
+
childrenToRender = Math.max(0, nChildren - 1);
|
| 168 |
+
} else {
|
| 169 |
+
childrenToRender = 0;
|
| 170 |
+
initialized = true;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
const convTreeStore = useConvTreeStore();
|
| 174 |
+
|
| 175 |
+
$: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
|
| 176 |
</script>
|
| 177 |
|
| 178 |
{#if message.from === "assistant"}
|
| 179 |
<div
|
| 180 |
+
class="group relative -mb-6 flex items-start justify-start gap-4 pb-4 leading-relaxed"
|
| 181 |
role="presentation"
|
| 182 |
on:click={() => (isTapped = !isTapped)}
|
| 183 |
on:keydown={() => (isTapped = !isTapped)}
|
|
|
|
| 204 |
webSearchMessages={searchUpdates}
|
| 205 |
/>
|
| 206 |
{/if}
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
<div
|
| 209 |
class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
|
|
|
| 218 |
{/if}
|
| 219 |
{/each}
|
| 220 |
</div>
|
| 221 |
+
|
| 222 |
<!-- Web Search sources -->
|
| 223 |
{#if webSearchSources?.length}
|
| 224 |
<div class="mt-4 flex flex-wrap items-center gap-x-2 gap-y-1.5 text-sm">
|
|
|
|
| 242 |
</div>
|
| 243 |
{#if isAuthor && !loading && message.content}
|
| 244 |
<div
|
| 245 |
+
class="absolute bottom-1 right-0 -mb-4 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
|
| 246 |
{message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
|
| 247 |
{isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
|
| 248 |
"
|
|
|
|
| 270 |
>
|
| 271 |
<CarbonThumbsDown class="h-[1.14em] w-[1.14em]" />
|
| 272 |
</button>
|
| 273 |
+
<button
|
| 274 |
+
class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300"
|
| 275 |
+
title="Retry"
|
| 276 |
+
type="button"
|
| 277 |
+
on:click={() => dispatch("retry", { id: message.id })}
|
| 278 |
+
>
|
| 279 |
+
<CarbonRotate360 />
|
| 280 |
+
</button>
|
| 281 |
<CopyToClipBoardBtn
|
| 282 |
on:click={() => {
|
| 283 |
isCopied = true;
|
|
|
|
| 288 |
</div>
|
| 289 |
{/if}
|
| 290 |
</div>
|
| 291 |
+
<slot name="childrenNav" />
|
| 292 |
{/if}
|
| 293 |
{#if message.from === "user"}
|
| 294 |
+
<div
|
| 295 |
+
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
|
| 296 |
+
role="presentation"
|
| 297 |
+
on:click={() => (isTapped = !isTapped)}
|
| 298 |
+
on:keydown={() => (isTapped = !isTapped)}
|
| 299 |
+
>
|
| 300 |
+
<div class="flex w-full flex-col">
|
| 301 |
{#if message.files && message.files.length > 0}
|
| 302 |
<div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
|
| 303 |
{#each message.files as file}
|
|
|
|
| 320 |
</div>
|
| 321 |
{/if}
|
| 322 |
|
| 323 |
+
<div class="flex w-full flex-row flex-nowrap">
|
| 324 |
+
{#if !editMode}
|
| 325 |
+
<p
|
| 326 |
+
class="disabled w-full appearance-none whitespace-break-spaces text-wrap break-words bg-inherit px-5 py-3.5 text-gray-500 dark:text-gray-400"
|
| 327 |
+
>
|
| 328 |
+
{message.content.trim()}
|
| 329 |
+
</p>
|
| 330 |
+
{:else}
|
| 331 |
+
<form
|
| 332 |
+
class="flex w-full flex-col"
|
| 333 |
+
bind:this={editFormEl}
|
| 334 |
+
on:submit|preventDefault={() => {
|
| 335 |
+
dispatch("retry", { content: editContentEl.value, id: message.id });
|
| 336 |
+
$convTreeStore.editing = null;
|
| 337 |
+
}}
|
| 338 |
+
>
|
| 339 |
+
<textarea
|
| 340 |
+
class="w-full whitespace-break-spaces break-words rounded-lg bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max dark:bg-gray-800 dark:text-gray-400"
|
| 341 |
+
bind:this={editContentEl}
|
| 342 |
+
value={message.content.trim()}
|
| 343 |
+
on:keydown={handleKeyDown}
|
| 344 |
+
required
|
| 345 |
+
/>
|
| 346 |
+
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
| 347 |
+
<button
|
| 348 |
+
type="submit"
|
| 349 |
+
class="btn rounded-lg px-3 py-1.5 text-sm
|
| 350 |
+
{loading
|
| 351 |
+
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
| 352 |
+
: 'bg-gray-200 text-gray-600 focus:ring-0 hover:text-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
| 353 |
+
"
|
| 354 |
+
disabled={loading}
|
| 355 |
+
>
|
| 356 |
+
Submit
|
| 357 |
+
</button>
|
| 358 |
+
<button
|
| 359 |
+
type="button"
|
| 360 |
+
class="btn rounded-sm p-2 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300"
|
| 361 |
+
on:click={() => {
|
| 362 |
+
$convTreeStore.editing = null;
|
| 363 |
+
}}
|
| 364 |
+
>
|
| 365 |
+
Cancel
|
| 366 |
+
</button>
|
| 367 |
+
</div>
|
| 368 |
+
</form>
|
| 369 |
+
{/if}
|
| 370 |
+
{#if !loading && !editMode}
|
| 371 |
+
<div
|
| 372 |
+
class="
|
| 373 |
+
max-md:opacity-0' invisible absolute
|
| 374 |
+
right-0 top-3.5 z-10 h-max max-md:-translate-y-4 max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100 {isTapped ||
|
| 375 |
+
isCopied
|
| 376 |
+
? 'max-md:visible max-md:translate-y-0 max-md:opacity-100'
|
| 377 |
+
: ''}"
|
| 378 |
+
>
|
| 379 |
+
<div class="mx-auto flex flex-row flex-nowrap gap-2">
|
| 380 |
+
{#if downloadLink}
|
| 381 |
+
<a
|
| 382 |
+
class="rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 max-sm:!hidden md:hidden dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
| 383 |
+
title="Download prompt and parameters"
|
| 384 |
+
type="button"
|
| 385 |
+
target="_blank"
|
| 386 |
+
href={downloadLink}
|
| 387 |
+
>
|
| 388 |
+
<CarbonDownload />
|
| 389 |
+
</a>
|
| 390 |
+
{/if}
|
| 391 |
+
{#if !readOnly}
|
| 392 |
+
<button
|
| 393 |
+
class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden lg:-right-2 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
| 394 |
+
title="Branch"
|
| 395 |
+
type="button"
|
| 396 |
+
on:click={() => ($convTreeStore.editing = message.id)}
|
| 397 |
+
>
|
| 398 |
+
<CarbonPen />
|
| 399 |
+
</button>
|
| 400 |
+
{/if}
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
{/if}
|
| 404 |
</div>
|
| 405 |
+
<slot name="childrenNav" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
</div>
|
| 407 |
</div>
|
| 408 |
{/if}
|
| 409 |
+
|
| 410 |
+
{#if nChildren > 0}
|
| 411 |
+
<svelte:self
|
| 412 |
+
{loading}
|
| 413 |
+
{messages}
|
| 414 |
+
{isAuthor}
|
| 415 |
+
{readOnly}
|
| 416 |
+
{model}
|
| 417 |
+
id={messages.find((m) => m.id === id)?.children?.[childrenToRender]}
|
| 418 |
+
on:retry
|
| 419 |
+
on:vote
|
| 420 |
+
on:continue
|
| 421 |
+
>
|
| 422 |
+
<svelte:fragment slot="childrenNav">
|
| 423 |
+
{#if nChildren > 1 && $convTreeStore.editing === null}
|
| 424 |
+
<div
|
| 425 |
+
class="font-white z-10 -mt-1 ml-3.5 mr-auto flex h-6 w-fit select-none flex-row items-center justify-center gap-1 text-sm"
|
| 426 |
+
>
|
| 427 |
+
<button
|
| 428 |
+
class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
|
| 429 |
+
on:click={() => (childrenToRender = Math.max(0, childrenToRender - 1))}
|
| 430 |
+
disabled={childrenToRender === 0 || loading}
|
| 431 |
+
>
|
| 432 |
+
<CarbonChevronLeft class="text-sm" />
|
| 433 |
+
</button>
|
| 434 |
+
<span class=" text-gray-400 dark:text-gray-500">
|
| 435 |
+
{childrenToRender + 1} / {nChildren}
|
| 436 |
+
</span>
|
| 437 |
+
<button
|
| 438 |
+
class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
|
| 439 |
+
on:click={() =>
|
| 440 |
+
(childrenToRender = Math.min(
|
| 441 |
+
message?.children?.length ?? 1 - 1,
|
| 442 |
+
childrenToRender + 1
|
| 443 |
+
))}
|
| 444 |
+
disabled={childrenToRender === nChildren - 1 || loading}
|
| 445 |
+
>
|
| 446 |
+
<CarbonChevronRight class="text-sm" />
|
| 447 |
+
</button>
|
| 448 |
+
</div>
|
| 449 |
+
{/if}
|
| 450 |
+
</svelte:fragment>
|
| 451 |
+
</svelte:self>
|
| 452 |
+
{/if}
|
|
@@ -1,106 +0,0 @@
|
|
| 1 |
-
<script lang="ts">
|
| 2 |
-
import type { Message } from "$lib/types/Message";
|
| 3 |
-
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
| 4 |
-
import ScrollToBottomBtn from "$lib/components/ScrollToBottomBtn.svelte";
|
| 5 |
-
import { tick } from "svelte";
|
| 6 |
-
import { randomUUID } from "$lib/utils/randomUuid";
|
| 7 |
-
import type { Model } from "$lib/types/Model";
|
| 8 |
-
import ChatIntroduction from "./ChatIntroduction.svelte";
|
| 9 |
-
import ChatMessage from "./ChatMessage.svelte";
|
| 10 |
-
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
| 11 |
-
import { browser } from "$app/environment";
|
| 12 |
-
import SystemPromptModal from "../SystemPromptModal.svelte";
|
| 13 |
-
import type { Assistant } from "$lib/types/Assistant";
|
| 14 |
-
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
| 15 |
-
import { page } from "$app/stores";
|
| 16 |
-
import { base } from "$app/paths";
|
| 17 |
-
|
| 18 |
-
export let messages: Message[];
|
| 19 |
-
export let loading: boolean;
|
| 20 |
-
export let pending: boolean;
|
| 21 |
-
export let isAuthor: boolean;
|
| 22 |
-
export let currentModel: Model;
|
| 23 |
-
export let assistant: Assistant | undefined;
|
| 24 |
-
export let models: Model[];
|
| 25 |
-
export let preprompt: string | undefined;
|
| 26 |
-
export let readOnly: boolean;
|
| 27 |
-
|
| 28 |
-
let chatContainer: HTMLElement;
|
| 29 |
-
|
| 30 |
-
export let webSearchMessages: WebSearchUpdate[] = [];
|
| 31 |
-
|
| 32 |
-
async function scrollToBottom() {
|
| 33 |
-
await tick();
|
| 34 |
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
// If last message is from user, scroll to bottom
|
| 38 |
-
$: if (browser && messages[messages.length - 1]?.from === "user") {
|
| 39 |
-
scrollToBottom();
|
| 40 |
-
}
|
| 41 |
-
</script>
|
| 42 |
-
|
| 43 |
-
<div
|
| 44 |
-
class="scrollbar-custom mr-1 h-full overflow-y-auto"
|
| 45 |
-
use:snapScrollToBottom={messages.length ? [...messages, ...webSearchMessages] : false}
|
| 46 |
-
bind:this={chatContainer}
|
| 47 |
-
>
|
| 48 |
-
<div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
|
| 49 |
-
{#each messages as message, i}
|
| 50 |
-
{#if i === 0 && $page.data?.assistant}
|
| 51 |
-
<a
|
| 52 |
-
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
| 53 |
-
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
| 54 |
-
>
|
| 55 |
-
{#if $page.data?.assistant.avatar}
|
| 56 |
-
<img
|
| 57 |
-
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
|
| 58 |
-
.data.assistant.avatar}"
|
| 59 |
-
alt="Avatar"
|
| 60 |
-
class="size-5 rounded-full object-cover"
|
| 61 |
-
/>
|
| 62 |
-
{:else}
|
| 63 |
-
<div
|
| 64 |
-
class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
|
| 65 |
-
>
|
| 66 |
-
{$page.data?.assistant.name[0]}
|
| 67 |
-
</div>
|
| 68 |
-
{/if}
|
| 69 |
-
|
| 70 |
-
{$page.data.assistant.name}
|
| 71 |
-
</a>
|
| 72 |
-
{:else if i === 0 && preprompt && preprompt != currentModel.preprompt}
|
| 73 |
-
<SystemPromptModal preprompt={preprompt ?? ""} />
|
| 74 |
-
{/if}
|
| 75 |
-
<ChatMessage
|
| 76 |
-
loading={loading && i === messages.length - 1}
|
| 77 |
-
{message}
|
| 78 |
-
{isAuthor}
|
| 79 |
-
{readOnly}
|
| 80 |
-
model={currentModel}
|
| 81 |
-
webSearchMessages={i === messages.length - 1 ? webSearchMessages : []}
|
| 82 |
-
on:retry
|
| 83 |
-
on:vote
|
| 84 |
-
on:continue
|
| 85 |
-
/>
|
| 86 |
-
{:else}
|
| 87 |
-
{#if !assistant}
|
| 88 |
-
<ChatIntroduction {models} {currentModel} on:message />
|
| 89 |
-
{:else}
|
| 90 |
-
<AssistantIntroduction {assistant} on:message />
|
| 91 |
-
{/if}
|
| 92 |
-
{/each}
|
| 93 |
-
{#if pending && messages[messages.length - 1]?.from === "user"}
|
| 94 |
-
<ChatMessage
|
| 95 |
-
message={{ from: "assistant", content: "", id: randomUUID() }}
|
| 96 |
-
model={currentModel}
|
| 97 |
-
{webSearchMessages}
|
| 98 |
-
/>
|
| 99 |
-
{/if}
|
| 100 |
-
<div class="h-44 flex-none" />
|
| 101 |
-
</div>
|
| 102 |
-
<ScrollToBottomBtn
|
| 103 |
-
class="bottom-36 right-4 max-md:hidden lg:right-10"
|
| 104 |
-
scrollNode={chatContainer}
|
| 105 |
-
/>
|
| 106 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import type { Message } from "$lib/types/Message";
|
| 3 |
-
import { createEventDispatcher, onDestroy } from "svelte";
|
| 4 |
|
| 5 |
import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
|
| 6 |
import CarbonExport from "~icons/carbon/export";
|
|
@@ -11,13 +11,11 @@
|
|
| 11 |
|
| 12 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
| 13 |
|
| 14 |
-
import ChatMessages from "./ChatMessages.svelte";
|
| 15 |
import ChatInput from "./ChatInput.svelte";
|
| 16 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
| 17 |
import type { Model } from "$lib/types/Model";
|
| 18 |
import WebSearchToggle from "../WebSearchToggle.svelte";
|
| 19 |
import LoginModal from "../LoginModal.svelte";
|
| 20 |
-
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
| 21 |
import { page } from "$app/stores";
|
| 22 |
import FileDropzone from "./FileDropzone.svelte";
|
| 23 |
import RetryBtn from "../RetryBtn.svelte";
|
|
@@ -26,15 +24,23 @@
|
|
| 26 |
import type { Assistant } from "$lib/types/Assistant";
|
| 27 |
import { base } from "$app/paths";
|
| 28 |
import ContinueBtn from "../ContinueBtn.svelte";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
export let messages: Message[] = [];
|
| 31 |
export let loading = false;
|
| 32 |
export let pending = false;
|
|
|
|
| 33 |
export let shared = false;
|
| 34 |
export let currentModel: Model;
|
| 35 |
export let models: Model[];
|
| 36 |
export let assistant: Assistant | undefined = undefined;
|
| 37 |
-
export let webSearchMessages: WebSearchUpdate[] = [];
|
| 38 |
export let preprompt: string | undefined = undefined;
|
| 39 |
export let files: File[] = [];
|
| 40 |
|
|
@@ -50,7 +56,7 @@
|
|
| 50 |
message: string;
|
| 51 |
share: void;
|
| 52 |
stop: void;
|
| 53 |
-
retry: { id: Message["id"]; content
|
| 54 |
continue: { id: Message["id"] };
|
| 55 |
}>();
|
| 56 |
|
|
@@ -76,7 +82,11 @@
|
|
| 76 |
const onDragOver = (e: DragEvent) => {
|
| 77 |
e.preventDefault();
|
| 78 |
};
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
$: sources = files.map((file) => file2base64(file));
|
| 82 |
|
|
@@ -96,6 +106,18 @@
|
|
| 96 |
clearTimeout(timeout);
|
| 97 |
}
|
| 98 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
</script>
|
| 100 |
|
| 101 |
<div class="relative min-h-0 min-w-0">
|
|
@@ -106,31 +128,79 @@
|
|
| 106 |
}}
|
| 107 |
/>
|
| 108 |
{/if}
|
| 109 |
-
<
|
| 110 |
-
|
| 111 |
-
{
|
| 112 |
-
{
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
<div
|
| 135 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
|
| 136 |
>
|
|
@@ -169,23 +239,28 @@
|
|
| 169 |
{:else if lastIsError}
|
| 170 |
<RetryBtn
|
| 171 |
classNames="ml-auto"
|
| 172 |
-
on:click={() =>
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
| 177 |
/>
|
| 178 |
{:else}
|
| 179 |
<div class="ml-auto gap-2">
|
| 180 |
{#if currentModel.multimodal}
|
| 181 |
<UploadBtn bind:files classNames="ml-auto" />
|
| 182 |
{/if}
|
| 183 |
-
{#if messages &&
|
| 184 |
<ContinueBtn
|
| 185 |
-
on:click={() =>
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
| 189 |
/>
|
| 190 |
{/if}
|
| 191 |
</div>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import type { Message } from "$lib/types/Message";
|
| 3 |
+
import { createEventDispatcher, onDestroy, tick } from "svelte";
|
| 4 |
|
| 5 |
import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
|
| 6 |
import CarbonExport from "~icons/carbon/export";
|
|
|
|
| 11 |
|
| 12 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
| 13 |
|
|
|
|
| 14 |
import ChatInput from "./ChatInput.svelte";
|
| 15 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
| 16 |
import type { Model } from "$lib/types/Model";
|
| 17 |
import WebSearchToggle from "../WebSearchToggle.svelte";
|
| 18 |
import LoginModal from "../LoginModal.svelte";
|
|
|
|
| 19 |
import { page } from "$app/stores";
|
| 20 |
import FileDropzone from "./FileDropzone.svelte";
|
| 21 |
import RetryBtn from "../RetryBtn.svelte";
|
|
|
|
| 24 |
import type { Assistant } from "$lib/types/Assistant";
|
| 25 |
import { base } from "$app/paths";
|
| 26 |
import ContinueBtn from "../ContinueBtn.svelte";
|
| 27 |
+
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
| 28 |
+
import ChatMessage from "./ChatMessage.svelte";
|
| 29 |
+
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
|
| 30 |
+
import { browser } from "$app/environment";
|
| 31 |
+
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
| 32 |
+
import SystemPromptModal from "../SystemPromptModal.svelte";
|
| 33 |
+
import ChatIntroduction from "./ChatIntroduction.svelte";
|
| 34 |
+
import { useConvTreeStore } from "$lib/stores/convTree";
|
| 35 |
|
| 36 |
export let messages: Message[] = [];
|
| 37 |
export let loading = false;
|
| 38 |
export let pending = false;
|
| 39 |
+
|
| 40 |
export let shared = false;
|
| 41 |
export let currentModel: Model;
|
| 42 |
export let models: Model[];
|
| 43 |
export let assistant: Assistant | undefined = undefined;
|
|
|
|
| 44 |
export let preprompt: string | undefined = undefined;
|
| 45 |
export let files: File[] = [];
|
| 46 |
|
|
|
|
| 56 |
message: string;
|
| 57 |
share: void;
|
| 58 |
stop: void;
|
| 59 |
+
retry: { id: Message["id"]; content?: string };
|
| 60 |
continue: { id: Message["id"] };
|
| 61 |
}>();
|
| 62 |
|
|
|
|
| 82 |
const onDragOver = (e: DragEvent) => {
|
| 83 |
e.preventDefault();
|
| 84 |
};
|
| 85 |
+
|
| 86 |
+
const convTreeStore = useConvTreeStore();
|
| 87 |
+
|
| 88 |
+
$: lastMessage = browser && (messages.find((m) => m.id == $convTreeStore.leaf) as Message);
|
| 89 |
+
$: lastIsError = lastMessage && lastMessage.from === "user" && !loading;
|
| 90 |
|
| 91 |
$: sources = files.map((file) => file2base64(file));
|
| 92 |
|
|
|
|
| 106 |
clearTimeout(timeout);
|
| 107 |
}
|
| 108 |
});
|
| 109 |
+
|
| 110 |
+
let chatContainer: HTMLElement;
|
| 111 |
+
|
| 112 |
+
async function scrollToBottom() {
|
| 113 |
+
await tick();
|
| 114 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// If last message is from user, scroll to bottom
|
| 118 |
+
$: if (lastMessage && lastMessage.from === "user") {
|
| 119 |
+
scrollToBottom();
|
| 120 |
+
}
|
| 121 |
</script>
|
| 122 |
|
| 123 |
<div class="relative min-h-0 min-w-0">
|
|
|
|
| 128 |
}}
|
| 129 |
/>
|
| 130 |
{/if}
|
| 131 |
+
<div
|
| 132 |
+
class="scrollbar-custom mr-1 h-full overflow-y-auto"
|
| 133 |
+
use:snapScrollToBottom={messages.length ? [...messages] : false}
|
| 134 |
+
bind:this={chatContainer}
|
| 135 |
+
>
|
| 136 |
+
<div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
|
| 137 |
+
{#if $page.data?.assistant}
|
| 138 |
+
<a
|
| 139 |
+
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
| 140 |
+
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
| 141 |
+
>
|
| 142 |
+
{#if $page.data?.assistant.avatar}
|
| 143 |
+
<img
|
| 144 |
+
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
|
| 145 |
+
.data.assistant.avatar}"
|
| 146 |
+
alt="Avatar"
|
| 147 |
+
class="size-5 rounded-full object-cover"
|
| 148 |
+
/>
|
| 149 |
+
{:else}
|
| 150 |
+
<div
|
| 151 |
+
class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
|
| 152 |
+
>
|
| 153 |
+
{$page.data?.assistant.name[0]}
|
| 154 |
+
</div>
|
| 155 |
+
{/if}
|
| 156 |
+
|
| 157 |
+
{$page.data.assistant.name}
|
| 158 |
+
</a>
|
| 159 |
+
{:else if preprompt && preprompt != currentModel.preprompt}
|
| 160 |
+
<SystemPromptModal preprompt={preprompt ?? ""} />
|
| 161 |
+
{/if}
|
| 162 |
|
| 163 |
+
{#if messages.length > 0}
|
| 164 |
+
<div class="flex h-max flex-col gap-6 pb-52">
|
| 165 |
+
<ChatMessage
|
| 166 |
+
{loading}
|
| 167 |
+
{messages}
|
| 168 |
+
id={messages[0].id}
|
| 169 |
+
isAuthor={!shared}
|
| 170 |
+
readOnly={isReadOnly}
|
| 171 |
+
model={currentModel}
|
| 172 |
+
on:retry
|
| 173 |
+
on:vote
|
| 174 |
+
on:continue
|
| 175 |
+
/>
|
| 176 |
+
</div>
|
| 177 |
+
{:else if pending}
|
| 178 |
+
<ChatMessage
|
| 179 |
+
loading={true}
|
| 180 |
+
messages={[
|
| 181 |
+
{
|
| 182 |
+
id: "0-0-0-0-0",
|
| 183 |
+
content: "",
|
| 184 |
+
from: "assistant",
|
| 185 |
+
children: [],
|
| 186 |
+
},
|
| 187 |
+
]}
|
| 188 |
+
id={"0-0-0-0-0"}
|
| 189 |
+
isAuthor={!shared}
|
| 190 |
+
readOnly={isReadOnly}
|
| 191 |
+
model={currentModel}
|
| 192 |
+
/>
|
| 193 |
+
{:else if !assistant}
|
| 194 |
+
<ChatIntroduction {models} {currentModel} on:message />
|
| 195 |
+
{:else}
|
| 196 |
+
<AssistantIntroduction {assistant} on:message />
|
| 197 |
+
{/if}
|
| 198 |
+
</div>
|
| 199 |
+
<ScrollToBottomBtn
|
| 200 |
+
class="bottom-36 right-4 max-md:hidden lg:right-10"
|
| 201 |
+
scrollNode={chatContainer}
|
| 202 |
+
/>
|
| 203 |
+
</div>
|
| 204 |
<div
|
| 205 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
|
| 206 |
>
|
|
|
|
| 239 |
{:else if lastIsError}
|
| 240 |
<RetryBtn
|
| 241 |
classNames="ml-auto"
|
| 242 |
+
on:click={() => {
|
| 243 |
+
if (lastMessage && lastMessage.ancestors) {
|
| 244 |
+
dispatch("retry", {
|
| 245 |
+
id: lastMessage.ancestors[lastMessage.ancestors.length - 1],
|
| 246 |
+
});
|
| 247 |
+
}
|
| 248 |
+
}}
|
| 249 |
/>
|
| 250 |
{:else}
|
| 251 |
<div class="ml-auto gap-2">
|
| 252 |
{#if currentModel.multimodal}
|
| 253 |
<UploadBtn bind:files classNames="ml-auto" />
|
| 254 |
{/if}
|
| 255 |
+
{#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
|
| 256 |
<ContinueBtn
|
| 257 |
+
on:click={() => {
|
| 258 |
+
if (lastMessage && lastMessage.ancestors) {
|
| 259 |
+
dispatch("continue", {
|
| 260 |
+
id: lastMessage?.id,
|
| 261 |
+
});
|
| 262 |
+
}
|
| 263 |
+
}}
|
| 264 |
/>
|
| 265 |
{/if}
|
| 266 |
</div>
|
|
@@ -62,6 +62,12 @@ client.on("open", () => {
|
|
| 62 |
{ partialFilterExpression: { userId: { $exists: true } } }
|
| 63 |
)
|
| 64 |
.catch(console.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
|
| 66 |
abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
|
| 67 |
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
|
|
|
|
| 62 |
{ partialFilterExpression: { userId: { $exists: true } } }
|
| 63 |
)
|
| 64 |
.catch(console.error);
|
| 65 |
+
conversations
|
| 66 |
+
.createIndex(
|
| 67 |
+
{ "message.id": 1, "message.ancestors": 1 },
|
| 68 |
+
{ partialFilterExpression: { userId: { $exists: true } } }
|
| 69 |
+
)
|
| 70 |
+
.catch(console.error);
|
| 71 |
abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
|
| 72 |
abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
|
| 73 |
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
|
|
@@ -36,11 +36,11 @@ export async function endpointAws(
|
|
| 36 |
region,
|
| 37 |
});
|
| 38 |
|
| 39 |
-
return async ({
|
| 40 |
const prompt = await buildPrompt({
|
| 41 |
-
messages
|
| 42 |
-
|
| 43 |
-
preprompt
|
| 44 |
model,
|
| 45 |
});
|
| 46 |
|
|
|
|
| 36 |
region,
|
| 37 |
});
|
| 38 |
|
| 39 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
| 40 |
const prompt = await buildPrompt({
|
| 41 |
+
messages,
|
| 42 |
+
continueMessage,
|
| 43 |
+
preprompt,
|
| 44 |
model,
|
| 45 |
});
|
| 46 |
|
|
@@ -8,13 +8,10 @@ import endpointLlamacpp, { endpointLlamacppParametersSchema } from "./llamacpp/e
|
|
| 8 |
import endpointOllama, { endpointOllamaParametersSchema } from "./ollama/endpointOllama";
|
| 9 |
|
| 10 |
// parameters passed when generating text
|
| 11 |
-
interface EndpointParameters {
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
_id?: Conversation["_id"];
|
| 16 |
-
};
|
| 17 |
-
continue?: boolean;
|
| 18 |
}
|
| 19 |
|
| 20 |
interface CommonEndpoint {
|
|
|
|
| 8 |
import endpointOllama, { endpointOllamaParametersSchema } from "./ollama/endpointOllama";
|
| 9 |
|
| 10 |
// parameters passed when generating text
|
| 11 |
+
export interface EndpointParameters {
|
| 12 |
+
messages: Omit<Conversation["messages"][0], "id">[];
|
| 13 |
+
preprompt?: Conversation["preprompt"];
|
| 14 |
+
continueMessage?: boolean; // used to signal that the last message will be extended
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
interface CommonEndpoint {
|
|
@@ -19,11 +19,11 @@ export function endpointLlamacpp(
|
|
| 19 |
input: z.input<typeof endpointLlamacppParametersSchema>
|
| 20 |
): Endpoint {
|
| 21 |
const { url, model } = endpointLlamacppParametersSchema.parse(input);
|
| 22 |
-
return async ({
|
| 23 |
const prompt = await buildPrompt({
|
| 24 |
-
messages
|
| 25 |
-
|
| 26 |
-
preprompt
|
| 27 |
model,
|
| 28 |
});
|
| 29 |
|
|
|
|
| 19 |
input: z.input<typeof endpointLlamacppParametersSchema>
|
| 20 |
): Endpoint {
|
| 21 |
const { url, model } = endpointLlamacppParametersSchema.parse(input);
|
| 22 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
| 23 |
const prompt = await buildPrompt({
|
| 24 |
+
messages,
|
| 25 |
+
continueMessage,
|
| 26 |
+
preprompt,
|
| 27 |
model,
|
| 28 |
});
|
| 29 |
|
|
@@ -14,11 +14,11 @@ export const endpointOllamaParametersSchema = z.object({
|
|
| 14 |
export function endpointOllama(input: z.input<typeof endpointOllamaParametersSchema>): Endpoint {
|
| 15 |
const { url, model, ollamaName } = endpointOllamaParametersSchema.parse(input);
|
| 16 |
|
| 17 |
-
return async ({
|
| 18 |
const prompt = await buildPrompt({
|
| 19 |
-
messages
|
| 20 |
-
|
| 21 |
-
preprompt
|
| 22 |
model,
|
| 23 |
});
|
| 24 |
|
|
|
|
| 14 |
export function endpointOllama(input: z.input<typeof endpointOllamaParametersSchema>): Endpoint {
|
| 15 |
const { url, model, ollamaName } = endpointOllamaParametersSchema.parse(input);
|
| 16 |
|
| 17 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
| 18 |
const prompt = await buildPrompt({
|
| 19 |
+
messages,
|
| 20 |
+
continueMessage,
|
| 21 |
+
preprompt,
|
| 22 |
model,
|
| 23 |
});
|
| 24 |
|
|
@@ -4,7 +4,6 @@ import { openAIChatToTextGenerationStream } from "./openAIChatToTextGenerationSt
|
|
| 4 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 5 |
import { OPENAI_API_KEY } from "$env/static/private";
|
| 6 |
import type { Endpoint } from "../endpoints";
|
| 7 |
-
import { format } from "date-fns";
|
| 8 |
|
| 9 |
export const endpointOAIParametersSchema = z.object({
|
| 10 |
weight: z.number().int().positive().default(1),
|
|
@@ -37,16 +36,18 @@ export async function endpointOai(
|
|
| 37 |
});
|
| 38 |
|
| 39 |
if (completion === "completions") {
|
| 40 |
-
return async ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
return openAICompletionToTextGenerationStream(
|
| 42 |
await openai.completions.create({
|
| 43 |
model: model.id ?? model.name,
|
| 44 |
-
prompt
|
| 45 |
-
messages: conversation.messages,
|
| 46 |
-
webSearch: conversation.messages[conversation.messages.length - 1].webSearch,
|
| 47 |
-
preprompt: conversation.preprompt,
|
| 48 |
-
model,
|
| 49 |
-
}),
|
| 50 |
stream: true,
|
| 51 |
max_tokens: model.parameters?.max_new_tokens,
|
| 52 |
stop: model.parameters?.stop,
|
|
@@ -57,48 +58,20 @@ export async function endpointOai(
|
|
| 57 |
);
|
| 58 |
};
|
| 59 |
} else if (completion === "chat_completions") {
|
| 60 |
-
return async ({
|
| 61 |
-
let
|
| 62 |
-
const webSearch = conversation.messages[conversation.messages.length - 1].webSearch;
|
| 63 |
-
|
| 64 |
-
if (webSearch && webSearch.context) {
|
| 65 |
-
const lastMsg = messages.slice(-1)[0];
|
| 66 |
-
const messagesWithoutLastUsrMsg = messages.slice(0, -1);
|
| 67 |
-
const previousUserMessages = messages.filter((el) => el.from === "user").slice(0, -1);
|
| 68 |
-
|
| 69 |
-
const previousQuestions =
|
| 70 |
-
previousUserMessages.length > 0
|
| 71 |
-
? `Previous questions: \n${previousUserMessages
|
| 72 |
-
.map(({ content }) => `- ${content}`)
|
| 73 |
-
.join("\n")}`
|
| 74 |
-
: "";
|
| 75 |
-
const currentDate = format(new Date(), "MMMM d, yyyy");
|
| 76 |
-
messages = [
|
| 77 |
-
...messagesWithoutLastUsrMsg,
|
| 78 |
-
{
|
| 79 |
-
from: "user",
|
| 80 |
-
content: `I searched the web using the query: ${webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
| 81 |
-
=====================
|
| 82 |
-
${webSearch.context}
|
| 83 |
-
=====================
|
| 84 |
-
${previousQuestions}
|
| 85 |
-
Answer the question: ${lastMsg.content}
|
| 86 |
-
`,
|
| 87 |
-
},
|
| 88 |
-
];
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
const messagesOpenAI = messages.map((message) => ({
|
| 92 |
role: message.from,
|
| 93 |
content: message.content,
|
| 94 |
}));
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return openAIChatToTextGenerationStream(
|
| 97 |
await openai.chat.completions.create({
|
| 98 |
model: model.id ?? model.name,
|
| 99 |
-
messages:
|
| 100 |
-
? [{ role: "system", content: conversation.preprompt }, ...messagesOpenAI]
|
| 101 |
-
: messagesOpenAI,
|
| 102 |
stream: true,
|
| 103 |
max_tokens: model.parameters?.max_new_tokens,
|
| 104 |
stop: model.parameters?.stop,
|
|
|
|
| 4 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 5 |
import { OPENAI_API_KEY } from "$env/static/private";
|
| 6 |
import type { Endpoint } from "../endpoints";
|
|
|
|
| 7 |
|
| 8 |
export const endpointOAIParametersSchema = z.object({
|
| 9 |
weight: z.number().int().positive().default(1),
|
|
|
|
| 36 |
});
|
| 37 |
|
| 38 |
if (completion === "completions") {
|
| 39 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
| 40 |
+
const prompt = await buildPrompt({
|
| 41 |
+
messages,
|
| 42 |
+
continueMessage,
|
| 43 |
+
preprompt,
|
| 44 |
+
model,
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
return openAICompletionToTextGenerationStream(
|
| 48 |
await openai.completions.create({
|
| 49 |
model: model.id ?? model.name,
|
| 50 |
+
prompt,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
stream: true,
|
| 52 |
max_tokens: model.parameters?.max_new_tokens,
|
| 53 |
stop: model.parameters?.stop,
|
|
|
|
| 58 |
);
|
| 59 |
};
|
| 60 |
} else if (completion === "chat_completions") {
|
| 61 |
+
return async ({ messages, preprompt }) => {
|
| 62 |
+
let messagesOpenAI = messages.map((message) => ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
role: message.from,
|
| 64 |
content: message.content,
|
| 65 |
}));
|
| 66 |
|
| 67 |
+
if (messagesOpenAI?.[0]?.role !== "system") {
|
| 68 |
+
messagesOpenAI = [{ role: "system", content: preprompt ?? "" }, ...messagesOpenAI];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
return openAIChatToTextGenerationStream(
|
| 72 |
await openai.chat.completions.create({
|
| 73 |
model: model.id ?? model.name,
|
| 74 |
+
messages: messagesOpenAI,
|
|
|
|
|
|
|
| 75 |
stream: true,
|
| 76 |
max_tokens: model.parameters?.max_new_tokens,
|
| 77 |
stop: model.parameters?.stop,
|
|
@@ -16,25 +16,14 @@ export const endpointTgiParametersSchema = z.object({
|
|
| 16 |
export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
|
| 17 |
const { url, accessToken, model, authorization } = endpointTgiParametersSchema.parse(input);
|
| 18 |
|
| 19 |
-
return async ({
|
| 20 |
-
|
| 21 |
-
messages
|
| 22 |
-
|
| 23 |
-
preprompt: conversation.preprompt,
|
| 24 |
model,
|
| 25 |
-
|
| 26 |
});
|
| 27 |
|
| 28 |
-
if (messageContinue) {
|
| 29 |
-
// start with the full prompt, and for each stop token, try to remove it from the end of the prompt
|
| 30 |
-
prompt = model.parameters.stop.reduce((acc: string, curr: string) => {
|
| 31 |
-
if (acc.endsWith(curr)) {
|
| 32 |
-
return acc.slice(0, acc.length - curr.length);
|
| 33 |
-
}
|
| 34 |
-
return acc;
|
| 35 |
-
}, prompt.trimEnd());
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
return textGenerationStream(
|
| 39 |
{
|
| 40 |
parameters: { ...model.parameters, return_full_text: false },
|
|
|
|
| 16 |
export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
|
| 17 |
const { url, accessToken, model, authorization } = endpointTgiParametersSchema.parse(input);
|
| 18 |
|
| 19 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
| 20 |
+
const prompt = await buildPrompt({
|
| 21 |
+
messages,
|
| 22 |
+
preprompt,
|
|
|
|
| 23 |
model,
|
| 24 |
+
continueMessage,
|
| 25 |
});
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
return textGenerationStream(
|
| 28 |
{
|
| 29 |
parameters: { ...model.parameters, return_full_text: false },
|
|
@@ -10,7 +10,7 @@ export async function generateFromDefaultEndpoint({
|
|
| 10 |
}): Promise<string> {
|
| 11 |
const endpoint = await smallModel.getEndpoint();
|
| 12 |
|
| 13 |
-
const tokenStream = await endpoint({
|
| 14 |
|
| 15 |
for await (const output of tokenStream) {
|
| 16 |
// if not generated_text is here it means the generation is not done
|
|
|
|
| 10 |
}): Promise<string> {
|
| 11 |
const endpoint = await smallModel.getEndpoint();
|
| 12 |
|
| 13 |
+
const tokenStream = await endpoint({ messages, preprompt });
|
| 14 |
|
| 15 |
for await (const output of tokenStream) {
|
| 16 |
// if not generated_text is here it means the generation is not done
|
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
| 2 |
+
import type { Message } from "$lib/types/Message";
|
| 3 |
+
import { format } from "date-fns";
|
| 4 |
+
import { downloadFile } from "./files/downloadFile";
|
| 5 |
+
|
| 6 |
+
export async function preprocessMessages(
|
| 7 |
+
messages: Message[],
|
| 8 |
+
multimodal: boolean,
|
| 9 |
+
id: Conversation["_id"]
|
| 10 |
+
): Promise<Message[]> {
|
| 11 |
+
return await Promise.all(
|
| 12 |
+
messages.map(async (message, idx) => {
|
| 13 |
+
// start by adding websearch to the last message
|
| 14 |
+
if (idx === messages.length - 1 && message.webSearch && message.webSearch.context) {
|
| 15 |
+
const lastUsrMsgIndex = messages.map((el) => el.from).lastIndexOf("user");
|
| 16 |
+
const previousUserMessages = messages.filter((el) => el.from === "user").slice(0, -1);
|
| 17 |
+
const previousQuestions =
|
| 18 |
+
previousUserMessages.length > 0
|
| 19 |
+
? `Previous questions: \n${previousUserMessages
|
| 20 |
+
.map(({ content }) => `- ${content}`)
|
| 21 |
+
.join("\n")}`
|
| 22 |
+
: "";
|
| 23 |
+
const currentDate = format(new Date(), "MMMM d, yyyy");
|
| 24 |
+
|
| 25 |
+
message.content = `I searched the web using the query: ${message.webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
| 26 |
+
=====================
|
| 27 |
+
${message.webSearch.context}
|
| 28 |
+
=====================
|
| 29 |
+
${previousQuestions}
|
| 30 |
+
Answer the question: ${messages[lastUsrMsgIndex].content}`;
|
| 31 |
+
}
|
| 32 |
+
// handle files if model is multimodal
|
| 33 |
+
if (multimodal) {
|
| 34 |
+
if (message.files && message.files.length > 0) {
|
| 35 |
+
const markdowns = await Promise.all(
|
| 36 |
+
message.files.map(async (hash) => {
|
| 37 |
+
try {
|
| 38 |
+
const { content: image, mime } = await downloadFile(hash, id);
|
| 39 |
+
const b64 = image.toString("base64");
|
| 40 |
+
return `})`;
|
| 41 |
+
} catch (e) {
|
| 42 |
+
console.error(e);
|
| 43 |
+
}
|
| 44 |
+
})
|
| 45 |
+
);
|
| 46 |
+
message.content += markdowns.join("\n ");
|
| 47 |
+
} else {
|
| 48 |
+
// if no image, append an empty white image
|
| 49 |
+
message.content +=
|
| 50 |
+
"\n";
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return message;
|
| 55 |
+
})
|
| 56 |
+
);
|
| 57 |
+
}
|
|
@@ -17,13 +17,10 @@ const DOMAIN_BLOCKLIST = ["youtube.com", "twitter.com"];
|
|
| 17 |
|
| 18 |
export async function runWebSearch(
|
| 19 |
conv: Conversation,
|
| 20 |
-
|
| 21 |
updatePad: (upd: MessageUpdate) => void
|
| 22 |
) {
|
| 23 |
-
const
|
| 24 |
-
return [...conv.messages, { content: prompt, from: "user", id: crypto.randomUUID() }];
|
| 25 |
-
})() satisfies Message[];
|
| 26 |
-
|
| 27 |
const webSearch: WebSearch = {
|
| 28 |
prompt,
|
| 29 |
searchQuery: "",
|
|
|
|
| 17 |
|
| 18 |
export async function runWebSearch(
|
| 19 |
conv: Conversation,
|
| 20 |
+
messages: Message[],
|
| 21 |
updatePad: (upd: MessageUpdate) => void
|
| 22 |
) {
|
| 23 |
+
const prompt = messages[messages.length - 1].content;
|
|
|
|
|
|
|
|
|
|
| 24 |
const webSearch: WebSearch = {
|
| 25 |
prompt,
|
| 26 |
searchQuery: "",
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Message } from "$lib/types/Message";
|
| 2 |
+
import { getContext, setContext } from "svelte";
|
| 3 |
+
import { writable, type Writable } from "svelte/store";
|
| 4 |
+
|
| 5 |
+
// used to store the id of the message that is the currently displayed leaf of the conversation tree
|
| 6 |
+
// (that is the last message in the current branch of the conversation tree)
|
| 7 |
+
|
| 8 |
+
interface ConvTreeStore {
|
| 9 |
+
leaf: Message["id"] | null;
|
| 10 |
+
editing: Message["id"] | null;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function useConvTreeStore() {
|
| 14 |
+
return getContext<Writable<ConvTreeStore>>("convTreeStore");
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function createConvTreeStore() {
|
| 18 |
+
const convTreeStore = writable<ConvTreeStore>({
|
| 19 |
+
leaf: null,
|
| 20 |
+
editing: null,
|
| 21 |
+
});
|
| 22 |
+
setContext("convTreeStore", convTreeStore);
|
| 23 |
+
|
| 24 |
+
return convTreeStore;
|
| 25 |
+
}
|
|
@@ -14,6 +14,7 @@ export interface Conversation extends Timestamps {
|
|
| 14 |
embeddingModel: string;
|
| 15 |
|
| 16 |
title: string;
|
|
|
|
| 17 |
messages: Message[];
|
| 18 |
|
| 19 |
meta?: {
|
|
|
|
| 14 |
embeddingModel: string;
|
| 15 |
|
| 16 |
title: string;
|
| 17 |
+
rootMessageId?: Message["id"];
|
| 18 |
messages: Message[];
|
| 19 |
|
| 20 |
meta?: {
|
|
@@ -3,7 +3,7 @@ import type { Timestamps } from "./Timestamps";
|
|
| 3 |
import type { WebSearch } from "./WebSearch";
|
| 4 |
|
| 5 |
export type Message = Partial<Timestamps> & {
|
| 6 |
-
from: "user" | "assistant";
|
| 7 |
id: ReturnType<typeof crypto.randomUUID>;
|
| 8 |
content: string;
|
| 9 |
updates?: MessageUpdate[];
|
|
@@ -12,4 +12,10 @@ export type Message = Partial<Timestamps> & {
|
|
| 12 |
score?: -1 | 0 | 1;
|
| 13 |
files?: string[]; // can contain either the hash of the file or the b64 encoded image data on the client side when uploading
|
| 14 |
interrupted?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
};
|
|
|
|
| 3 |
import type { WebSearch } from "./WebSearch";
|
| 4 |
|
| 5 |
export type Message = Partial<Timestamps> & {
|
| 6 |
+
from: "user" | "assistant" | "system";
|
| 7 |
id: ReturnType<typeof crypto.randomUUID>;
|
| 8 |
content: string;
|
| 9 |
updates?: MessageUpdate[];
|
|
|
|
| 12 |
score?: -1 | 0 | 1;
|
| 13 |
files?: string[]; // can contain either the hash of the file or the b64 encoded image data on the client side when uploading
|
| 14 |
interrupted?: boolean;
|
| 15 |
+
|
| 16 |
+
// needed for conversation trees
|
| 17 |
+
ancestors?: Message["id"][];
|
| 18 |
+
|
| 19 |
+
// goes one level deep
|
| 20 |
+
children?: Message["id"][];
|
| 21 |
};
|
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
import type {
|
| 2 |
-
import type { Message } from "./Message";
|
| 3 |
-
import type { Timestamps } from "./Timestamps";
|
| 4 |
|
| 5 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
_id: string;
|
| 7 |
-
|
| 8 |
hash: string;
|
| 9 |
-
|
| 10 |
-
model: string;
|
| 11 |
-
embeddingModel: string;
|
| 12 |
-
|
| 13 |
-
title: string;
|
| 14 |
-
messages: Message[];
|
| 15 |
-
preprompt?: string;
|
| 16 |
-
assistantId?: Assistant["_id"];
|
| 17 |
-
}
|
|
|
|
| 1 |
+
import type { Conversation } from "./Conversation";
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
export type SharedConversation = Pick<
|
| 4 |
+
Conversation,
|
| 5 |
+
| "model"
|
| 6 |
+
| "embeddingModel"
|
| 7 |
+
| "title"
|
| 8 |
+
| "rootMessageId"
|
| 9 |
+
| "messages"
|
| 10 |
+
| "preprompt"
|
| 11 |
+
| "assistantId"
|
| 12 |
+
| "createdAt"
|
| 13 |
+
| "updatedAt"
|
| 14 |
+
> & {
|
| 15 |
_id: string;
|
|
|
|
| 16 |
hash: string;
|
| 17 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { ObjectId } from "mongodb";
|
| 3 |
+
import { describe, expect, it } from "vitest";
|
| 4 |
+
|
| 5 |
+
import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
|
| 6 |
+
import { addChildren } from "./addChildren";
|
| 7 |
+
import type { Message } from "$lib/types/Message";
|
| 8 |
+
|
| 9 |
+
const newMessage: Omit<Message, "id"> = {
|
| 10 |
+
content: "new message",
|
| 11 |
+
from: "user",
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
Object.freeze(newMessage);
|
| 15 |
+
|
| 16 |
+
describe("addChildren", async () => {
|
| 17 |
+
it("should let you append on legacy conversations", async () => {
|
| 18 |
+
const convId = await insertLegacyConversation();
|
| 19 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 20 |
+
if (!conv) throw new Error("Conversation not found");
|
| 21 |
+
|
| 22 |
+
const convLength = conv.messages.length;
|
| 23 |
+
|
| 24 |
+
addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id);
|
| 25 |
+
expect(conv.messages.length).toEqual(convLength + 1);
|
| 26 |
+
});
|
| 27 |
+
it("should not let you create branches on legacy conversations", async () => {
|
| 28 |
+
const convId = await insertLegacyConversation();
|
| 29 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 30 |
+
if (!conv) throw new Error("Conversation not found");
|
| 31 |
+
|
| 32 |
+
expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow();
|
| 33 |
+
});
|
| 34 |
+
it("should not let you create a message that already exists", async () => {
|
| 35 |
+
const convId = await insertLegacyConversation();
|
| 36 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 37 |
+
if (!conv) throw new Error("Conversation not found");
|
| 38 |
+
|
| 39 |
+
const messageThatAlreadyExists: Message = {
|
| 40 |
+
id: conv.messages[0].id,
|
| 41 |
+
content: "new message",
|
| 42 |
+
from: "user",
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
expect(() => addChildren(conv, messageThatAlreadyExists, conv.messages[0].id)).toThrow();
|
| 46 |
+
});
|
| 47 |
+
it("should let you create branches on conversations with subtrees", async () => {
|
| 48 |
+
const convId = await insertSideBranchesConversation();
|
| 49 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 50 |
+
if (!conv) throw new Error("Conversation not found");
|
| 51 |
+
|
| 52 |
+
const nChildren = conv.messages[0].children?.length;
|
| 53 |
+
if (!nChildren) throw new Error("No children found");
|
| 54 |
+
addChildren(conv, newMessage, conv.messages[0].id);
|
| 55 |
+
expect(conv.messages[0].children?.length).toEqual(nChildren + 1);
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
it("should let you create a new leaf", async () => {
|
| 59 |
+
const convId = await insertSideBranchesConversation();
|
| 60 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 61 |
+
if (!conv) throw new Error("Conversation not found");
|
| 62 |
+
|
| 63 |
+
const parentId = conv.messages[conv.messages.length - 1].id;
|
| 64 |
+
const nChildren = conv.messages[conv.messages.length - 1].children?.length;
|
| 65 |
+
|
| 66 |
+
if (nChildren === undefined) throw new Error("No children found");
|
| 67 |
+
expect(nChildren).toEqual(0);
|
| 68 |
+
|
| 69 |
+
addChildren(conv, newMessage, parentId);
|
| 70 |
+
expect(conv.messages[conv.messages.length - 2].children?.length).toEqual(nChildren + 1);
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
it("should let you append to an empty conversation without specifying a parentId", async () => {
|
| 74 |
+
const conv = {
|
| 75 |
+
_id: new ObjectId(),
|
| 76 |
+
rootMessageId: undefined,
|
| 77 |
+
messages: [] as Message[],
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
addChildren(conv, newMessage);
|
| 81 |
+
expect(conv.messages.length).toEqual(1);
|
| 82 |
+
expect(conv.rootMessageId).toEqual(conv.messages[0].id);
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
it("should throw if you don't specify a parentId in a conversation with messages", async () => {
|
| 86 |
+
const convId = await insertLegacyConversation();
|
| 87 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 88 |
+
if (!conv) throw new Error("Conversation not found");
|
| 89 |
+
|
| 90 |
+
expect(() => addChildren(conv, newMessage)).toThrow();
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
it("should return the id of the new message", async () => {
|
| 94 |
+
const convId = await insertLegacyConversation();
|
| 95 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 96 |
+
if (!conv) throw new Error("Conversation not found");
|
| 97 |
+
|
| 98 |
+
expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual(
|
| 99 |
+
conv.messages[conv.messages.length - 1].id
|
| 100 |
+
);
|
| 101 |
+
});
|
| 102 |
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
| 2 |
+
import type { Message } from "$lib/types/Message";
|
| 3 |
+
|
| 4 |
+
export function addChildren(
|
| 5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
| 6 |
+
message: Omit<Message, "id">,
|
| 7 |
+
parentId?: Message["id"]
|
| 8 |
+
): Message["id"] {
|
| 9 |
+
// if this is the first message we just push it
|
| 10 |
+
if (conv.messages.length === 0) {
|
| 11 |
+
const messageId = crypto.randomUUID();
|
| 12 |
+
conv.rootMessageId = messageId;
|
| 13 |
+
conv.messages.push({
|
| 14 |
+
...message,
|
| 15 |
+
ancestors: [],
|
| 16 |
+
id: messageId,
|
| 17 |
+
});
|
| 18 |
+
return messageId;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (!parentId) {
|
| 22 |
+
throw new Error("You need to specify a parentId if this is not the first message");
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const messageId = crypto.randomUUID();
|
| 26 |
+
if (!conv.rootMessageId) {
|
| 27 |
+
// if there is no parentId we just push the message
|
| 28 |
+
if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
|
| 29 |
+
throw new Error("This is a legacy conversation, you can only append to the last message");
|
| 30 |
+
}
|
| 31 |
+
conv.messages.push({ ...message, id: messageId });
|
| 32 |
+
return messageId;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId];
|
| 36 |
+
conv.messages.push({
|
| 37 |
+
...message,
|
| 38 |
+
ancestors,
|
| 39 |
+
id: messageId,
|
| 40 |
+
children: [],
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
const parent = conv.messages.find((m) => m.id === parentId);
|
| 44 |
+
|
| 45 |
+
if (parent) {
|
| 46 |
+
if (parent.children) {
|
| 47 |
+
parent.children.push(messageId);
|
| 48 |
+
} else parent.children = [messageId];
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return messageId;
|
| 52 |
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { ObjectId } from "mongodb";
|
| 3 |
+
import { describe, expect, it } from "vitest";
|
| 4 |
+
|
| 5 |
+
import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
|
| 6 |
+
import type { Message } from "$lib/types/Message";
|
| 7 |
+
import { addSibling } from "./addSibling";
|
| 8 |
+
|
| 9 |
+
const newMessage: Omit<Message, "id"> = {
|
| 10 |
+
content: "new message",
|
| 11 |
+
from: "user",
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
Object.freeze(newMessage);
|
| 15 |
+
|
| 16 |
+
describe("addSibling", async () => {
|
| 17 |
+
it("should fail on empty conversations", () => {
|
| 18 |
+
const conv = {
|
| 19 |
+
_id: new ObjectId(),
|
| 20 |
+
rootMessageId: undefined,
|
| 21 |
+
messages: [],
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
|
| 25 |
+
"Cannot add a sibling to an empty conversation"
|
| 26 |
+
);
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
it("should fail on legacy conversations", async () => {
|
| 30 |
+
const convId = await insertLegacyConversation();
|
| 31 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 32 |
+
if (!conv) throw new Error("Conversation not found");
|
| 33 |
+
|
| 34 |
+
expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow(
|
| 35 |
+
"Cannot add a sibling to a legacy conversation"
|
| 36 |
+
);
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
it("should fail if the sibling message doesn't exist", async () => {
|
| 40 |
+
const convId = await insertSideBranchesConversation();
|
| 41 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 42 |
+
if (!conv) throw new Error("Conversation not found");
|
| 43 |
+
|
| 44 |
+
expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
|
| 45 |
+
"The sibling message doesn't exist"
|
| 46 |
+
);
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
// TODO: This behaviour should be fixed, we do not need to fail on the root message.
|
| 50 |
+
it("should fail if the sibling message is the root message", async () => {
|
| 51 |
+
const convId = await insertSideBranchesConversation();
|
| 52 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 53 |
+
if (!conv) throw new Error("Conversation not found");
|
| 54 |
+
if (!conv.rootMessageId) throw new Error("Root message not found");
|
| 55 |
+
|
| 56 |
+
expect(() => addSibling(conv, newMessage, conv.rootMessageId as Message["id"])).toThrow(
|
| 57 |
+
"The sibling message is the root message, therefore we can't add a sibling"
|
| 58 |
+
);
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
it("should add a sibling to a message", async () => {
|
| 62 |
+
const convId = await insertSideBranchesConversation();
|
| 63 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 64 |
+
if (!conv) throw new Error("Conversation not found");
|
| 65 |
+
|
| 66 |
+
// add sibling and check children count for parnets
|
| 67 |
+
|
| 68 |
+
const nChildren = conv.messages[1].children?.length;
|
| 69 |
+
const siblingId = addSibling(conv, newMessage, conv.messages[2].id);
|
| 70 |
+
const nChildrenNew = conv.messages[1].children?.length;
|
| 71 |
+
|
| 72 |
+
if (!nChildren) throw new Error("No children found");
|
| 73 |
+
|
| 74 |
+
expect(nChildrenNew).toBe(nChildren + 1);
|
| 75 |
+
|
| 76 |
+
// make sure siblings have the same ancestors
|
| 77 |
+
const sibling = conv.messages.find((m) => m.id === siblingId);
|
| 78 |
+
expect(sibling?.ancestors).toEqual(conv.messages[2].ancestors);
|
| 79 |
+
});
|
| 80 |
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
| 2 |
+
import type { Message } from "$lib/types/Message";
|
| 3 |
+
|
| 4 |
+
export function addSibling(
|
| 5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
| 6 |
+
message: Omit<Message, "id">,
|
| 7 |
+
siblingId: Message["id"]
|
| 8 |
+
): Message["id"] {
|
| 9 |
+
if (conv.messages.length === 0) {
|
| 10 |
+
throw new Error("Cannot add a sibling to an empty conversation");
|
| 11 |
+
}
|
| 12 |
+
if (!conv.rootMessageId) {
|
| 13 |
+
throw new Error("Cannot add a sibling to a legacy conversation");
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const sibling = conv.messages.find((m) => m.id === siblingId);
|
| 17 |
+
|
| 18 |
+
if (!sibling) {
|
| 19 |
+
throw new Error("The sibling message doesn't exist");
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
if (!sibling.ancestors || sibling.ancestors?.length === 0) {
|
| 23 |
+
throw new Error("The sibling message is the root message, therefore we can't add a sibling");
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const messageId = crypto.randomUUID();
|
| 27 |
+
|
| 28 |
+
conv.messages.push({
|
| 29 |
+
...message,
|
| 30 |
+
id: messageId,
|
| 31 |
+
ancestors: sibling.ancestors,
|
| 32 |
+
children: [],
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1];
|
| 36 |
+
const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId);
|
| 37 |
+
|
| 38 |
+
if (nearestAncestor) {
|
| 39 |
+
if (nearestAncestor.children) {
|
| 40 |
+
nearestAncestor.children.push(messageId);
|
| 41 |
+
} else nearestAncestor.children = [messageId];
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return messageId;
|
| 45 |
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { ObjectId } from "mongodb";
|
| 3 |
+
import { describe, expect, it } from "vitest";
|
| 4 |
+
|
| 5 |
+
import {
|
| 6 |
+
insertLegacyConversation,
|
| 7 |
+
insertLinearBranchConversation,
|
| 8 |
+
insertSideBranchesConversation,
|
| 9 |
+
} from "./treeHelpers.spec";
|
| 10 |
+
import { buildSubtree } from "./buildSubtree";
|
| 11 |
+
|
| 12 |
+
describe("buildSubtree", () => {
|
| 13 |
+
it("a subtree in a legacy conversation should be just a slice", async () => {
|
| 14 |
+
const convId = await insertLegacyConversation();
|
| 15 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 16 |
+
if (!conv) throw new Error("Conversation not found");
|
| 17 |
+
|
| 18 |
+
// check middle
|
| 19 |
+
const id = conv.messages[2].id;
|
| 20 |
+
const subtree = buildSubtree(conv, id);
|
| 21 |
+
expect(subtree).toEqual(conv.messages.slice(0, 3));
|
| 22 |
+
|
| 23 |
+
// check zero
|
| 24 |
+
const id2 = conv.messages[0].id;
|
| 25 |
+
const subtree2 = buildSubtree(conv, id2);
|
| 26 |
+
expect(subtree2).toEqual(conv.messages.slice(0, 1));
|
| 27 |
+
|
| 28 |
+
//check full length
|
| 29 |
+
const id3 = conv.messages[conv.messages.length - 1].id;
|
| 30 |
+
const subtree3 = buildSubtree(conv, id3);
|
| 31 |
+
expect(subtree3).toEqual(conv.messages);
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
it("a subtree in a linear branch conversation should be the ancestors and the message", async () => {
|
| 35 |
+
const convId = await insertLinearBranchConversation();
|
| 36 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 37 |
+
if (!conv) throw new Error("Conversation not found");
|
| 38 |
+
|
| 39 |
+
// check middle
|
| 40 |
+
const id = conv.messages[1].id;
|
| 41 |
+
const subtree = buildSubtree(conv, id);
|
| 42 |
+
expect(subtree).toEqual([conv.messages[0], conv.messages[1]]);
|
| 43 |
+
|
| 44 |
+
// check zero
|
| 45 |
+
const id2 = conv.messages[0].id;
|
| 46 |
+
const subtree2 = buildSubtree(conv, id2);
|
| 47 |
+
expect(subtree2).toEqual([conv.messages[0]]);
|
| 48 |
+
|
| 49 |
+
//check full length
|
| 50 |
+
const id3 = conv.messages[conv.messages.length - 1].id;
|
| 51 |
+
const subtree3 = buildSubtree(conv, id3);
|
| 52 |
+
expect(subtree3).toEqual(conv.messages);
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
it("should throw an error if the message is not found", async () => {
|
| 56 |
+
const convId = await insertLinearBranchConversation();
|
| 57 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 58 |
+
if (!conv) throw new Error("Conversation not found");
|
| 59 |
+
|
| 60 |
+
const id = "not-a-real-id-test";
|
| 61 |
+
|
| 62 |
+
expect(() => buildSubtree(conv, id)).toThrow("Message not found");
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
it("should throw an error if the ancestor is not found", async () => {
|
| 66 |
+
const convId = await insertLinearBranchConversation();
|
| 67 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 68 |
+
if (!conv) throw new Error("Conversation not found");
|
| 69 |
+
|
| 70 |
+
const id = "1-1-1-1-2";
|
| 71 |
+
|
| 72 |
+
conv.messages[1].ancestors = ["not-a-real-id-test"];
|
| 73 |
+
|
| 74 |
+
expect(() => buildSubtree(conv, id)).toThrow("Ancestor not found");
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
it("should work on empty conversations", () => {
|
| 78 |
+
const conv = {
|
| 79 |
+
_id: new ObjectId(),
|
| 80 |
+
rootMessageId: undefined,
|
| 81 |
+
messages: [],
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
const subtree = buildSubtree(conv, "not-a-real-id-test");
|
| 85 |
+
expect(subtree).toEqual([]);
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
it("should work for conversation with subtrees", async () => {
|
| 89 |
+
const convId = await insertSideBranchesConversation();
|
| 90 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 91 |
+
if (!conv) throw new Error("Conversation not found");
|
| 92 |
+
|
| 93 |
+
const subtree = buildSubtree(conv, "1-1-1-1-2");
|
| 94 |
+
expect(subtree).toEqual([conv.messages[0], conv.messages[1]]);
|
| 95 |
+
|
| 96 |
+
const subtree2 = buildSubtree(conv, "1-1-1-1-4");
|
| 97 |
+
expect(subtree2).toEqual([
|
| 98 |
+
conv.messages[0],
|
| 99 |
+
conv.messages[1],
|
| 100 |
+
conv.messages[2],
|
| 101 |
+
conv.messages[3],
|
| 102 |
+
]);
|
| 103 |
+
|
| 104 |
+
const subtree3 = buildSubtree(conv, "1-1-1-1-6");
|
| 105 |
+
expect(subtree3).toEqual([conv.messages[0], conv.messages[4], conv.messages[5]]);
|
| 106 |
+
|
| 107 |
+
const subtree4 = buildSubtree(conv, "1-1-1-1-7");
|
| 108 |
+
expect(subtree4).toEqual([conv.messages[0], conv.messages[4], conv.messages[6]]);
|
| 109 |
+
});
|
| 110 |
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
| 2 |
+
import type { Message } from "$lib/types/Message";
|
| 3 |
+
|
| 4 |
+
export function buildSubtree(
|
| 5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
| 6 |
+
id: Message["id"]
|
| 7 |
+
): Message[] {
|
| 8 |
+
if (!conv.rootMessageId) {
|
| 9 |
+
if (conv.messages.length === 0) return [];
|
| 10 |
+
// legacy conversation slice up to id
|
| 11 |
+
const index = conv.messages.findIndex((m) => m.id === id);
|
| 12 |
+
if (index === -1) throw new Error("Message not found");
|
| 13 |
+
return conv.messages.slice(0, index + 1);
|
| 14 |
+
} else {
|
| 15 |
+
// find the message with the right id then create the ancestor tree
|
| 16 |
+
const message = conv.messages.find((m) => m.id === id);
|
| 17 |
+
if (!message) throw new Error("Message not found");
|
| 18 |
+
|
| 19 |
+
return [
|
| 20 |
+
...(message.ancestors?.map((ancestorId) => {
|
| 21 |
+
const ancestor = conv.messages.find((m) => m.id === ancestorId);
|
| 22 |
+
if (!ancestor) throw new Error("Ancestor not found");
|
| 23 |
+
return ancestor;
|
| 24 |
+
}) ?? []),
|
| 25 |
+
message,
|
| 26 |
+
];
|
| 27 |
+
}
|
| 28 |
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { ObjectId } from "mongodb";
|
| 3 |
+
import { describe, expect, it } from "vitest";
|
| 4 |
+
|
| 5 |
+
import { convertLegacyConversation } from "./convertLegacyConversation";
|
| 6 |
+
import { insertLegacyConversation } from "./treeHelpers.spec";
|
| 7 |
+
|
| 8 |
+
describe("convertLegacyConversation", () => {
|
| 9 |
+
it("should convert a legacy conversation", async () => {
|
| 10 |
+
const convId = await insertLegacyConversation();
|
| 11 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
| 12 |
+
if (!conv) throw new Error("Conversation not found");
|
| 13 |
+
|
| 14 |
+
const newConv = convertLegacyConversation(conv);
|
| 15 |
+
|
| 16 |
+
expect(newConv.rootMessageId).toBe(newConv.messages[0].id);
|
| 17 |
+
expect(newConv.messages[0].ancestors).toEqual([]);
|
| 18 |
+
expect(newConv.messages[1].ancestors).toEqual([newConv.messages[0].id]);
|
| 19 |
+
expect(newConv.messages[0].children).toEqual([newConv.messages[1].id]);
|
| 20 |
+
});
|
| 21 |
+
it("should work on empty conversations", async () => {
|
| 22 |
+
const conv = {
|
| 23 |
+
_id: new ObjectId(),
|
| 24 |
+
rootMessageId: undefined,
|
| 25 |
+
messages: [],
|
| 26 |
+
};
|
| 27 |
+
const newConv = convertLegacyConversation(conv);
|
| 28 |
+
expect(newConv.rootMessageId).toBe(undefined);
|
| 29 |
+
expect(newConv.messages).toEqual([]);
|
| 30 |
+
});
|
| 31 |
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
| 2 |
+
import type { Message } from "$lib/types/Message";
|
| 3 |
+
|
| 4 |
+
export function convertLegacyConversation(
|
| 5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId" | "preprompt">
|
| 6 |
+
): Pick<Conversation, "messages" | "rootMessageId" | "preprompt"> {
|
| 7 |
+
if (conv.rootMessageId) return conv; // not a legacy conversation
|
| 8 |
+
if (conv.messages.length === 0) return conv; // empty conversation
|
| 9 |
+
const messages = [
|
| 10 |
+
{
|
| 11 |
+
from: "system",
|
| 12 |
+
content: conv.preprompt ?? "",
|
| 13 |
+
createdAt: new Date(),
|
| 14 |
+
updatedAt: new Date(),
|
| 15 |
+
id: crypto.randomUUID(),
|
| 16 |
+
} satisfies Message,
|
| 17 |
+
...conv.messages,
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
const rootMessageId = messages[0].id;
|
| 21 |
+
|
| 22 |
+
const newMessages = messages.map((message, index) => {
|
| 23 |
+
return {
|
| 24 |
+
...message,
|
| 25 |
+
ancestors: messages.slice(0, index).map((m) => m.id),
|
| 26 |
+
children: index < messages.length - 1 ? [messages[index + 1].id] : [],
|
| 27 |
+
};
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
...conv,
|
| 32 |
+
rootMessageId,
|
| 33 |
+
messages: newMessages,
|
| 34 |
+
};
|
| 35 |
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from "vitest";
|
| 2 |
+
import { isMessageId } from "./isMessageId";
|
| 3 |
+
|
| 4 |
+
describe("isMessageId", () => {
|
| 5 |
+
it("should return true for a valid message id", () => {
|
| 6 |
+
expect(isMessageId(crypto.randomUUID())).toBe(true);
|
| 7 |
+
});
|
| 8 |
+
it("should return false for an invalid message id", () => {
|
| 9 |
+
expect(isMessageId("1-2-3-4")).toBe(false);
|
| 10 |
+
});
|
| 11 |
+
it("should return false for an empty string", () => {
|
| 12 |
+
expect(isMessageId("")).toBe(false);
|
| 13 |
+
});
|
| 14 |
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Message } from "$lib/types/Message";
|
| 2 |
+
|
| 3 |
+
export function isMessageId(id: string): id is Message["id"] {
|
| 4 |
+
return id.split("-").length === 5;
|
| 5 |
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { ObjectId } from "mongodb";
|
| 3 |
+
import { describe, expect, it } from "vitest";
|
| 4 |
+
|
| 5 |
+
// function used to insert conversations used for testing
|
| 6 |
+
|
| 7 |
+
export const insertLegacyConversation = async () => {
|
| 8 |
+
const res = await collections.conversations.insertOne({
|
| 9 |
+
_id: new ObjectId(),
|
| 10 |
+
createdAt: new Date(),
|
| 11 |
+
updatedAt: new Date(),
|
| 12 |
+
title: "legacy conversation",
|
| 13 |
+
model: "",
|
| 14 |
+
embeddingModel: "",
|
| 15 |
+
messages: [
|
| 16 |
+
{
|
| 17 |
+
id: "1-1-1-1-1",
|
| 18 |
+
from: "user",
|
| 19 |
+
content: "Hello, world! I am a user",
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: "1-1-1-1-2",
|
| 23 |
+
from: "assistant",
|
| 24 |
+
content: "Hello, world! I am an assistant.",
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
id: "1-1-1-1-3",
|
| 28 |
+
from: "user",
|
| 29 |
+
content: "Hello, world! I am a user.",
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
id: "1-1-1-1-4",
|
| 33 |
+
from: "assistant",
|
| 34 |
+
content: "Hello, world! I am an assistant.",
|
| 35 |
+
},
|
| 36 |
+
],
|
| 37 |
+
});
|
| 38 |
+
return res.insertedId;
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
export const insertLinearBranchConversation = async () => {
|
| 42 |
+
const res = await collections.conversations.insertOne({
|
| 43 |
+
_id: new ObjectId(),
|
| 44 |
+
createdAt: new Date(),
|
| 45 |
+
updatedAt: new Date(),
|
| 46 |
+
title: "linear branch conversation",
|
| 47 |
+
model: "",
|
| 48 |
+
embeddingModel: "",
|
| 49 |
+
|
| 50 |
+
rootMessageId: "1-1-1-1-1",
|
| 51 |
+
messages: [
|
| 52 |
+
{
|
| 53 |
+
id: "1-1-1-1-1",
|
| 54 |
+
from: "user",
|
| 55 |
+
content: "Hello, world! I am a user",
|
| 56 |
+
ancestors: [],
|
| 57 |
+
children: ["1-1-1-1-2"],
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: "1-1-1-1-2",
|
| 61 |
+
from: "assistant",
|
| 62 |
+
content: "Hello, world! I am an assistant.",
|
| 63 |
+
ancestors: ["1-1-1-1-1"],
|
| 64 |
+
children: ["1-1-1-1-3"],
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: "1-1-1-1-3",
|
| 68 |
+
from: "user",
|
| 69 |
+
content: "Hello, world! I am a user.",
|
| 70 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2"],
|
| 71 |
+
children: ["1-1-1-1-4"],
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
id: "1-1-1-1-4",
|
| 75 |
+
from: "assistant",
|
| 76 |
+
content: "Hello, world! I am an assistant.",
|
| 77 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"],
|
| 78 |
+
children: [],
|
| 79 |
+
},
|
| 80 |
+
],
|
| 81 |
+
});
|
| 82 |
+
return res.insertedId;
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
export const insertSideBranchesConversation = async () => {
|
| 86 |
+
const res = await collections.conversations.insertOne({
|
| 87 |
+
_id: new ObjectId(),
|
| 88 |
+
createdAt: new Date(),
|
| 89 |
+
updatedAt: new Date(),
|
| 90 |
+
title: "side branches conversation",
|
| 91 |
+
model: "",
|
| 92 |
+
embeddingModel: "",
|
| 93 |
+
rootMessageId: "1-1-1-1-1",
|
| 94 |
+
messages: [
|
| 95 |
+
{
|
| 96 |
+
id: "1-1-1-1-1",
|
| 97 |
+
from: "user",
|
| 98 |
+
content: "Hello, world, root message!",
|
| 99 |
+
ancestors: [],
|
| 100 |
+
children: ["1-1-1-1-2", "1-1-1-1-5"],
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
id: "1-1-1-1-2",
|
| 104 |
+
from: "assistant",
|
| 105 |
+
content: "Hello, response to root message!",
|
| 106 |
+
ancestors: ["1-1-1-1-1"],
|
| 107 |
+
children: ["1-1-1-1-3"],
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
id: "1-1-1-1-3",
|
| 111 |
+
from: "user",
|
| 112 |
+
content: "Hello, follow up question!",
|
| 113 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2"],
|
| 114 |
+
children: ["1-1-1-1-4"],
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
id: "1-1-1-1-4",
|
| 118 |
+
from: "assistant",
|
| 119 |
+
content: "Hello, response from follow up question!",
|
| 120 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"],
|
| 121 |
+
children: [],
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
id: "1-1-1-1-5",
|
| 125 |
+
from: "assistant",
|
| 126 |
+
content: "Hello, alternative assistant answer!",
|
| 127 |
+
ancestors: ["1-1-1-1-1"],
|
| 128 |
+
children: ["1-1-1-1-6", "1-1-1-1-7"],
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
id: "1-1-1-1-6",
|
| 132 |
+
from: "user",
|
| 133 |
+
content: "Hello, follow up question to alternative answer!",
|
| 134 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-5"],
|
| 135 |
+
children: [],
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
id: "1-1-1-1-7",
|
| 139 |
+
from: "user",
|
| 140 |
+
content: "Hello, alternative follow up question to alternative answer!",
|
| 141 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-5"],
|
| 142 |
+
children: [],
|
| 143 |
+
},
|
| 144 |
+
],
|
| 145 |
+
});
|
| 146 |
+
return res.insertedId;
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
describe("inserting conversations", () => {
|
| 150 |
+
it("should insert a legacy conversation", async () => {
|
| 151 |
+
const id = await insertLegacyConversation();
|
| 152 |
+
expect(id).toBeDefined();
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
it("should insert a linear branch conversation", async () => {
|
| 156 |
+
const id = await insertLinearBranchConversation();
|
| 157 |
+
expect(id).toBeDefined();
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
it("should insert a side branches conversation", async () => {
|
| 161 |
+
const id = await insertSideBranchesConversation();
|
| 162 |
+
expect(id).toBeDefined();
|
| 163 |
+
});
|
| 164 |
+
});
|
|
@@ -12,7 +12,6 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
| 12 |
const body = await request.text();
|
| 13 |
|
| 14 |
let title = "";
|
| 15 |
-
let messages: Message[] = [];
|
| 16 |
|
| 17 |
const values = z
|
| 18 |
.object({
|
|
@@ -23,6 +22,19 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
| 23 |
})
|
| 24 |
.parse(JSON.parse(body));
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
let embeddingModel: string;
|
| 27 |
|
| 28 |
if (values.fromShare) {
|
|
@@ -36,6 +48,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
| 36 |
|
| 37 |
title = conversation.title;
|
| 38 |
messages = conversation.messages;
|
|
|
|
| 39 |
values.model = conversation.model;
|
| 40 |
values.preprompt = conversation.preprompt;
|
| 41 |
values.assistantId = conversation.assistantId?.toString();
|
|
@@ -69,6 +82,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
| 69 |
const res = await collections.conversations.insertOne({
|
| 70 |
_id: new ObjectId(),
|
| 71 |
title: title || "New Chat",
|
|
|
|
| 72 |
messages,
|
| 73 |
model: values.model,
|
| 74 |
preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
|
|
|
|
| 12 |
const body = await request.text();
|
| 13 |
|
| 14 |
let title = "";
|
|
|
|
| 15 |
|
| 16 |
const values = z
|
| 17 |
.object({
|
|
|
|
| 22 |
})
|
| 23 |
.parse(JSON.parse(body));
|
| 24 |
|
| 25 |
+
let messages: Message[] = [
|
| 26 |
+
{
|
| 27 |
+
id: crypto.randomUUID(),
|
| 28 |
+
from: "system",
|
| 29 |
+
content: values.preprompt ?? "",
|
| 30 |
+
createdAt: new Date(),
|
| 31 |
+
updatedAt: new Date(),
|
| 32 |
+
children: [],
|
| 33 |
+
ancestors: [],
|
| 34 |
+
},
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
let rootMessageId: Message["id"] = messages[0].id;
|
| 38 |
let embeddingModel: string;
|
| 39 |
|
| 40 |
if (values.fromShare) {
|
|
|
|
| 48 |
|
| 49 |
title = conversation.title;
|
| 50 |
messages = conversation.messages;
|
| 51 |
+
rootMessageId = conversation.rootMessageId ?? rootMessageId;
|
| 52 |
values.model = conversation.model;
|
| 53 |
values.preprompt = conversation.preprompt;
|
| 54 |
values.assistantId = conversation.assistantId?.toString();
|
|
|
|
| 82 |
const res = await collections.conversations.insertOne({
|
| 83 |
_id: new ObjectId(),
|
| 84 |
title: title || "New Chat",
|
| 85 |
+
rootMessageId,
|
| 86 |
messages,
|
| 87 |
model: values.model,
|
| 88 |
preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
|
|
@@ -3,6 +3,7 @@ import { ObjectId } from "mongodb";
|
|
| 3 |
import { error } from "@sveltejs/kit";
|
| 4 |
import { authCondition } from "$lib/server/auth";
|
| 5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
|
|
|
| 6 |
|
| 7 |
export const load = async ({ params, depends, locals }) => {
|
| 8 |
let conversation;
|
|
@@ -45,16 +46,19 @@ export const load = async ({ params, depends, locals }) => {
|
|
| 45 |
}
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
| 48 |
return {
|
| 49 |
-
messages:
|
| 50 |
-
title:
|
| 51 |
-
model:
|
| 52 |
-
preprompt:
|
| 53 |
-
|
|
|
|
| 54 |
? JSON.parse(
|
| 55 |
JSON.stringify(
|
| 56 |
await collections.assistants.findOne({
|
| 57 |
-
_id: new ObjectId(
|
| 58 |
})
|
| 59 |
)
|
| 60 |
)
|
|
|
|
| 3 |
import { error } from "@sveltejs/kit";
|
| 4 |
import { authCondition } from "$lib/server/auth";
|
| 5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
| 6 |
+
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation.js";
|
| 7 |
|
| 8 |
export const load = async ({ params, depends, locals }) => {
|
| 9 |
let conversation;
|
|
|
|
| 46 |
}
|
| 47 |
}
|
| 48 |
|
| 49 |
+
const convertedConv = { ...conversation, ...convertLegacyConversation(conversation) };
|
| 50 |
+
|
| 51 |
return {
|
| 52 |
+
messages: convertedConv.messages,
|
| 53 |
+
title: convertedConv.title,
|
| 54 |
+
model: convertedConv.model,
|
| 55 |
+
preprompt: convertedConv.preprompt,
|
| 56 |
+
rootMessageId: convertedConv.rootMessageId,
|
| 57 |
+
assistant: convertedConv.assistantId
|
| 58 |
? JSON.parse(
|
| 59 |
JSON.stringify(
|
| 60 |
await collections.assistants.findOne({
|
| 61 |
+
_id: new ObjectId(convertedConv.assistantId),
|
| 62 |
})
|
| 63 |
)
|
| 64 |
)
|
|
@@ -9,20 +9,21 @@
|
|
| 9 |
import { shareConversation } from "$lib/shareConversation";
|
| 10 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
| 11 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
| 12 |
-
import { randomUUID } from "$lib/utils/randomUuid";
|
| 13 |
import { findCurrentModel } from "$lib/utils/models";
|
| 14 |
import { webSearchParameters } from "$lib/stores/webSearchParameters";
|
| 15 |
import type { Message } from "$lib/types/Message";
|
| 16 |
-
import type { MessageUpdate
|
| 17 |
import titleUpdate from "$lib/stores/titleUpdate";
|
| 18 |
import file2base64 from "$lib/utils/file2base64";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
export let data;
|
| 20 |
|
| 21 |
let messages = data.messages;
|
| 22 |
let lastLoadedMessages = data.messages;
|
| 23 |
|
| 24 |
-
let webSearchMessages: WebSearchUpdate[] = [];
|
| 25 |
-
|
| 26 |
// Since we modify the messages array locally, we don't want to reset it if an old version is passed
|
| 27 |
$: if (data.messages !== lastLoadedMessages) {
|
| 28 |
messages = data.messages;
|
|
@@ -66,12 +67,12 @@
|
|
| 66 |
// this function is used to send new message to the backends
|
| 67 |
async function writeMessage({
|
| 68 |
prompt,
|
| 69 |
-
messageId =
|
| 70 |
isRetry = false,
|
| 71 |
isContinue = false,
|
| 72 |
}: {
|
| 73 |
prompt?: string;
|
| 74 |
-
messageId?: ReturnType<typeof randomUUID>;
|
| 75 |
isRetry?: boolean;
|
| 76 |
isContinue?: boolean;
|
| 77 |
}): Promise<void> {
|
|
@@ -80,25 +81,7 @@
|
|
| 80 |
loading = true;
|
| 81 |
pending = true;
|
| 82 |
|
| 83 |
-
// first we check if the messageId already exists, indicating a retry
|
| 84 |
-
|
| 85 |
-
let msgIndex = messages.findIndex((msg) => msg.id === messageId);
|
| 86 |
-
|
| 87 |
-
if (msgIndex === -1) {
|
| 88 |
-
msgIndex = messages.length - 1;
|
| 89 |
-
}
|
| 90 |
-
if (isRetry && messages[msgIndex].from === "assistant") {
|
| 91 |
-
throw new Error("Trying to retry a message that is not from user");
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
if (isContinue && messages[msgIndex].from === "user") {
|
| 95 |
-
throw new Error("Trying to continue a message that is not from assistant");
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
// const isNewMessage = !isRetry && !isContinue;
|
| 99 |
-
|
| 100 |
const module = await import("browser-image-resizer");
|
| 101 |
-
|
| 102 |
// currently, only IDEFICS is supported by TGI
|
| 103 |
// the size of images is hardcoded to 224x224 in TGI
|
| 104 |
// this will need to be configurable when support for more models is added
|
|
@@ -114,35 +97,101 @@
|
|
| 114 |
})
|
| 115 |
);
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
{
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
id: messageId,
|
| 125 |
-
files: messages[msgIndex].files,
|
| 126 |
},
|
| 127 |
-
];
|
| 128 |
-
} else if (!isContinue) {
|
| 129 |
-
// or add a new message if its not a continue request
|
| 130 |
-
if (!prompt) {
|
| 131 |
-
throw new Error("Prompt is undefined");
|
| 132 |
-
}
|
| 133 |
-
messages = [
|
| 134 |
-
...messages,
|
| 135 |
{
|
| 136 |
from: "user",
|
| 137 |
content: prompt ?? "",
|
| 138 |
-
id: messageId,
|
| 139 |
files: resizedImages,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
},
|
| 141 |
-
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
|
|
|
| 145 |
|
|
|
|
|
|
|
|
|
|
| 146 |
// disable websearch if assistant is present
|
| 147 |
const hasAssistant = !!$page.data.assistant;
|
| 148 |
|
|
@@ -221,25 +270,16 @@
|
|
| 221 |
invalidate(UrlDependency.Conversation);
|
| 222 |
} else if (update.type === "stream") {
|
| 223 |
pending = false;
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
if (lastMessage.from !== "assistant") {
|
| 228 |
-
messages = [
|
| 229 |
-
...messages,
|
| 230 |
-
{ from: "assistant", id: randomUUID(), content: update.token },
|
| 231 |
-
];
|
| 232 |
-
} else {
|
| 233 |
-
lastMessage.content += update.token;
|
| 234 |
-
messages = [...messages];
|
| 235 |
-
}
|
| 236 |
} else if (update.type === "webSearch") {
|
| 237 |
-
|
|
|
|
| 238 |
} else if (update.type === "status") {
|
| 239 |
if (update.status === "title" && update.message) {
|
| 240 |
-
const
|
| 241 |
-
if (
|
| 242 |
-
|
| 243 |
|
| 244 |
$titleUpdate = {
|
| 245 |
title: update.message,
|
|
@@ -265,11 +305,7 @@
|
|
| 265 |
});
|
| 266 |
}
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
const lastMessage = messages[messages.length - 1];
|
| 271 |
-
lastMessage.updates = messageUpdates;
|
| 272 |
-
|
| 273 |
await invalidate(UrlDependency.ConversationList);
|
| 274 |
} catch (err) {
|
| 275 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
|
@@ -285,6 +321,7 @@
|
|
| 285 |
} finally {
|
| 286 |
loading = false;
|
| 287 |
pending = false;
|
|
|
|
| 288 |
}
|
| 289 |
}
|
| 290 |
|
|
@@ -336,7 +373,7 @@
|
|
| 336 |
}
|
| 337 |
}
|
| 338 |
|
| 339 |
-
async function onRetry(event: CustomEvent<{ id: Message["id"]; content
|
| 340 |
if (!data.shared) {
|
| 341 |
await writeMessage({
|
| 342 |
prompt: event.detail.content,
|
|
@@ -363,11 +400,26 @@
|
|
| 363 |
async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
|
| 364 |
if (!data.shared) {
|
| 365 |
writeMessage({ messageId: event.detail.id, isContinue: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
| 367 |
}
|
| 368 |
|
| 369 |
-
$: $page.params.id, (($isAborted = true), (loading = false));
|
| 370 |
$: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
|
|
|
|
|
|
|
| 371 |
</script>
|
| 372 |
|
| 373 |
<svelte:head>
|
|
@@ -386,7 +438,6 @@
|
|
| 386 |
{messages}
|
| 387 |
shared={data.shared}
|
| 388 |
preprompt={data.preprompt}
|
| 389 |
-
bind:webSearchMessages
|
| 390 |
bind:files
|
| 391 |
on:message={onMessage}
|
| 392 |
on:retry={onRetry}
|
|
|
|
| 9 |
import { shareConversation } from "$lib/shareConversation";
|
| 10 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
| 11 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
|
|
|
| 12 |
import { findCurrentModel } from "$lib/utils/models";
|
| 13 |
import { webSearchParameters } from "$lib/stores/webSearchParameters";
|
| 14 |
import type { Message } from "$lib/types/Message";
|
| 15 |
+
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
| 16 |
import titleUpdate from "$lib/stores/titleUpdate";
|
| 17 |
import file2base64 from "$lib/utils/file2base64";
|
| 18 |
+
import { addChildren } from "$lib/utils/tree/addChildren";
|
| 19 |
+
import { addSibling } from "$lib/utils/tree/addSibling";
|
| 20 |
+
import { createConvTreeStore } from "$lib/stores/convTree";
|
| 21 |
+
|
| 22 |
export let data;
|
| 23 |
|
| 24 |
let messages = data.messages;
|
| 25 |
let lastLoadedMessages = data.messages;
|
| 26 |
|
|
|
|
|
|
|
| 27 |
// Since we modify the messages array locally, we don't want to reset it if an old version is passed
|
| 28 |
$: if (data.messages !== lastLoadedMessages) {
|
| 29 |
messages = data.messages;
|
|
|
|
| 67 |
// this function is used to send new message to the backends
|
| 68 |
async function writeMessage({
|
| 69 |
prompt,
|
| 70 |
+
messageId = $convTreeStore.leaf ?? undefined,
|
| 71 |
isRetry = false,
|
| 72 |
isContinue = false,
|
| 73 |
}: {
|
| 74 |
prompt?: string;
|
| 75 |
+
messageId?: ReturnType<typeof crypto.randomUUID>;
|
| 76 |
isRetry?: boolean;
|
| 77 |
isContinue?: boolean;
|
| 78 |
}): Promise<void> {
|
|
|
|
| 81 |
loading = true;
|
| 82 |
pending = true;
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
const module = await import("browser-image-resizer");
|
|
|
|
| 85 |
// currently, only IDEFICS is supported by TGI
|
| 86 |
// the size of images is hardcoded to 224x224 in TGI
|
| 87 |
// this will need to be configurable when support for more models is added
|
|
|
|
| 97 |
})
|
| 98 |
);
|
| 99 |
|
| 100 |
+
let messageToWriteToId: Message["id"] | undefined = undefined;
|
| 101 |
+
// used for building the prompt, subtree of the conversation that goes from the latest message to the root
|
| 102 |
+
|
| 103 |
+
if (isContinue && messageId) {
|
| 104 |
+
if ((messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) {
|
| 105 |
+
$error = "Can only continue the last message";
|
| 106 |
+
} else {
|
| 107 |
+
messageToWriteToId = messageId;
|
| 108 |
+
}
|
| 109 |
+
} else if (isRetry && messageId) {
|
| 110 |
+
// two cases, if we're retrying a user message with a newPrompt set,
|
| 111 |
+
// it means we're editing a user message
|
| 112 |
+
// if we're retrying on an assistant message, newPrompt cannot be set
|
| 113 |
+
// it means we're retrying the last assistant message for a new answer
|
| 114 |
+
|
| 115 |
+
const messageToRetry = messages.find((message) => message.id === messageId);
|
| 116 |
+
|
| 117 |
+
if (!messageToRetry) {
|
| 118 |
+
$error = "Message not found";
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (messageToRetry?.from === "user" && prompt) {
|
| 122 |
+
// add a sibling to this message from the user, with the alternative prompt
|
| 123 |
+
// add a children to that sibling, where we can write to
|
| 124 |
+
const newUserMessageId = addSibling(
|
| 125 |
+
{
|
| 126 |
+
messages,
|
| 127 |
+
rootMessageId: data.rootMessageId,
|
| 128 |
+
},
|
| 129 |
+
{ from: "user", content: prompt },
|
| 130 |
+
messageId
|
| 131 |
+
);
|
| 132 |
+
messageToWriteToId = addChildren(
|
| 133 |
+
{
|
| 134 |
+
messages,
|
| 135 |
+
rootMessageId: data.rootMessageId,
|
| 136 |
+
},
|
| 137 |
+
{ from: "assistant", content: "", files: resizedImages },
|
| 138 |
+
newUserMessageId
|
| 139 |
+
);
|
| 140 |
+
} else if (messageToRetry?.from === "assistant") {
|
| 141 |
+
// we're retrying an assistant message, to generate a new answer
|
| 142 |
+
// just add a sibling to the assistant answer where we can write to
|
| 143 |
+
messageToWriteToId = addSibling(
|
| 144 |
+
{
|
| 145 |
+
messages,
|
| 146 |
+
rootMessageId: data.rootMessageId,
|
| 147 |
+
},
|
| 148 |
+
{ from: "assistant", content: "" },
|
| 149 |
+
messageId
|
| 150 |
+
);
|
| 151 |
+
}
|
| 152 |
+
} else {
|
| 153 |
+
// just a normal linear conversation, so we add the user message
|
| 154 |
+
// and the blank assistant message back to back
|
| 155 |
+
const newUserMessageId = addChildren(
|
| 156 |
{
|
| 157 |
+
messages,
|
| 158 |
+
rootMessageId: data.rootMessageId,
|
|
|
|
|
|
|
| 159 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
{
|
| 161 |
from: "user",
|
| 162 |
content: prompt ?? "",
|
|
|
|
| 163 |
files: resizedImages,
|
| 164 |
+
createdAt: new Date(),
|
| 165 |
+
updatedAt: new Date(),
|
| 166 |
+
},
|
| 167 |
+
messageId
|
| 168 |
+
);
|
| 169 |
+
|
| 170 |
+
if (!data.rootMessageId) {
|
| 171 |
+
data.rootMessageId = newUserMessageId;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
messageToWriteToId = addChildren(
|
| 175 |
+
{
|
| 176 |
+
messages,
|
| 177 |
+
rootMessageId: data.rootMessageId,
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
from: "assistant",
|
| 181 |
+
content: "",
|
| 182 |
+
createdAt: new Date(),
|
| 183 |
+
updatedAt: new Date(),
|
| 184 |
},
|
| 185 |
+
newUserMessageId
|
| 186 |
+
);
|
| 187 |
}
|
| 188 |
|
| 189 |
+
messages = [...messages];
|
| 190 |
+
const messageToWriteTo = messages.find((message) => message.id === messageToWriteToId);
|
| 191 |
|
| 192 |
+
if (!messageToWriteTo) {
|
| 193 |
+
throw new Error("Message to write to not found");
|
| 194 |
+
}
|
| 195 |
// disable websearch if assistant is present
|
| 196 |
const hasAssistant = !!$page.data.assistant;
|
| 197 |
|
|
|
|
| 270 |
invalidate(UrlDependency.Conversation);
|
| 271 |
} else if (update.type === "stream") {
|
| 272 |
pending = false;
|
| 273 |
+
messageToWriteTo.content += update.token;
|
| 274 |
+
messages = [...messages];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
} else if (update.type === "webSearch") {
|
| 276 |
+
messageToWriteTo.updates = [...(messageToWriteTo.updates ?? []), update];
|
| 277 |
+
messages = [...messages];
|
| 278 |
} else if (update.type === "status") {
|
| 279 |
if (update.status === "title" && update.message) {
|
| 280 |
+
const convInData = data.conversations.find(({ id }) => id === $page.params.id);
|
| 281 |
+
if (convInData) {
|
| 282 |
+
convInData.title = update.message;
|
| 283 |
|
| 284 |
$titleUpdate = {
|
| 285 |
title: update.message,
|
|
|
|
| 305 |
});
|
| 306 |
}
|
| 307 |
|
| 308 |
+
messageToWriteTo.updates = messageUpdates;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
await invalidate(UrlDependency.ConversationList);
|
| 310 |
} catch (err) {
|
| 311 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
|
|
|
| 321 |
} finally {
|
| 322 |
loading = false;
|
| 323 |
pending = false;
|
| 324 |
+
await invalidate(UrlDependency.Conversation);
|
| 325 |
}
|
| 326 |
}
|
| 327 |
|
|
|
|
| 373 |
}
|
| 374 |
}
|
| 375 |
|
| 376 |
+
async function onRetry(event: CustomEvent<{ id: Message["id"]; content?: string }>) {
|
| 377 |
if (!data.shared) {
|
| 378 |
await writeMessage({
|
| 379 |
prompt: event.detail.content,
|
|
|
|
| 400 |
async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
|
| 401 |
if (!data.shared) {
|
| 402 |
writeMessage({ messageId: event.detail.id, isContinue: true });
|
| 403 |
+
} else {
|
| 404 |
+
await convFromShared()
|
| 405 |
+
.then(async (convId) => {
|
| 406 |
+
await goto(`${base}/conversation/${convId}`, { invalidateAll: true });
|
| 407 |
+
})
|
| 408 |
+
.then(
|
| 409 |
+
async () =>
|
| 410 |
+
await writeMessage({
|
| 411 |
+
messageId: event.detail.id,
|
| 412 |
+
isContinue: true,
|
| 413 |
+
})
|
| 414 |
+
)
|
| 415 |
+
.finally(() => (loading = false));
|
| 416 |
}
|
| 417 |
}
|
| 418 |
|
| 419 |
+
$: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
|
| 420 |
$: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
|
| 421 |
+
|
| 422 |
+
const convTreeStore = createConvTreeStore();
|
| 423 |
</script>
|
| 424 |
|
| 425 |
<svelte:head>
|
|
|
|
| 438 |
{messages}
|
| 439 |
shared={data.shared}
|
| 440 |
preprompt={data.preprompt}
|
|
|
|
| 441 |
bind:files
|
| 442 |
on:message={onMessage}
|
| 443 |
on:retry={onRetry}
|
|
@@ -9,11 +9,16 @@ import { ObjectId } from "mongodb";
|
|
| 9 |
import { z } from "zod";
|
| 10 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
| 11 |
import { runWebSearch } from "$lib/server/websearch/runWebSearch";
|
| 12 |
-
import type { WebSearch } from "$lib/types/WebSearch";
|
| 13 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
| 14 |
import { summarize } from "$lib/server/summarize";
|
| 15 |
import { uploadFile } from "$lib/server/files/uploadFile";
|
| 16 |
import sizeof from "image-size";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
export async function POST({ request, locals, params, getClientAddress }) {
|
| 19 |
const id = z.string().parse(params.id);
|
|
@@ -28,6 +33,29 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 28 |
}
|
| 29 |
|
| 30 |
// check if the user has access to the conversation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
const conv = await collections.conversations.findOne({
|
| 32 |
_id: convId,
|
| 33 |
...authCondition(locals),
|
|
@@ -97,8 +125,8 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 97 |
files: b64files,
|
| 98 |
} = z
|
| 99 |
.object({
|
|
|
|
| 100 |
inputs: z.optional(z.string().trim().min(1)),
|
| 101 |
-
id: z.optional(z.string().uuid()),
|
| 102 |
is_retry: z.optional(z.boolean()),
|
| 103 |
is_continue: z.optional(z.boolean()),
|
| 104 |
web_search: z.optional(z.boolean()),
|
|
@@ -138,59 +166,93 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 138 |
hashes = await Promise.all(files.map(async (file) => await uploadFile(file, conv)));
|
| 139 |
}
|
| 140 |
|
| 141 |
-
//
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
} else {
|
| 172 |
-
// in normal conversation we add an extra user message
|
| 173 |
-
return [
|
| 174 |
-
...conv.messages,
|
| 175 |
-
{
|
| 176 |
-
content: newPrompt ?? "",
|
| 177 |
-
from: "user",
|
| 178 |
-
id: (messageId as Message["id"]) || crypto.randomUUID(),
|
| 179 |
-
createdAt: new Date(),
|
| 180 |
-
updatedAt: new Date(),
|
| 181 |
-
files: hashes,
|
| 182 |
-
},
|
| 183 |
-
];
|
| 184 |
-
} // else append the message at the bottom
|
| 185 |
-
})() satisfies Message[];
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
await collections.conversations.updateOne(
|
| 188 |
{
|
| 189 |
_id: convId,
|
| 190 |
},
|
| 191 |
{
|
| 192 |
$set: {
|
| 193 |
-
messages,
|
| 194 |
title: conv.title,
|
| 195 |
updatedAt: new Date(),
|
| 196 |
},
|
|
@@ -202,13 +264,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 202 |
// we now build the stream
|
| 203 |
const stream = new ReadableStream({
|
| 204 |
async start(controller) {
|
| 205 |
-
|
| 206 |
-
? conv.messages[conv.messages.length - 1].updates ?? []
|
| 207 |
-
: [];
|
| 208 |
-
|
| 209 |
function update(newUpdate: MessageUpdate) {
|
| 210 |
if (newUpdate.type !== "stream") {
|
| 211 |
-
updates
|
| 212 |
}
|
| 213 |
|
| 214 |
if (newUpdate.type === "stream" && newUpdate.token === "") {
|
|
@@ -225,10 +284,21 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 225 |
update({ type: "status", status: "started" });
|
| 226 |
|
| 227 |
const summarizeIfNeeded = (async () => {
|
| 228 |
-
if (conv.title === "New Chat" && messages.length ===
|
| 229 |
try {
|
| 230 |
-
conv.title = (await summarize(messages[
|
| 231 |
update({ type: "status", status: "title", message: conv.title });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
} catch (e) {
|
| 233 |
console.error(e);
|
| 234 |
}
|
|
@@ -241,31 +311,33 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 241 |
},
|
| 242 |
{
|
| 243 |
$set: {
|
| 244 |
-
messages,
|
| 245 |
title: conv.title,
|
| 246 |
updatedAt: new Date(),
|
| 247 |
},
|
| 248 |
}
|
| 249 |
);
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
if (webSearch && !isContinue && !conv.assistantId) {
|
| 254 |
-
|
| 255 |
-
messages[messages.length - 1].webSearch = webSearchResults;
|
| 256 |
-
} else if (isContinue) {
|
| 257 |
-
webSearchResults = messages[messages.length - 1].webSearch;
|
| 258 |
}
|
| 259 |
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
const
|
| 263 |
-
? conv.messages.find((message) => message.id === messageId)?.content ?? ""
|
| 264 |
-
: "";
|
| 265 |
|
| 266 |
try {
|
| 267 |
const endpoint = await model.getEndpoint();
|
| 268 |
-
for await (const output of await endpoint({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
// if not generated_text is here it means the generation is not done
|
| 270 |
if (!output.generated_text) {
|
| 271 |
// else we get the next token
|
|
@@ -274,63 +346,33 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 274 |
type: "stream",
|
| 275 |
token: output.token.text,
|
| 276 |
});
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
// id doesn't match the backend id but it's not important for assistant messages
|
| 286 |
-
// First token has a space at the beginning, trim it
|
| 287 |
-
{
|
| 288 |
-
from: "assistant",
|
| 289 |
-
content: output.token.text.trimStart(),
|
| 290 |
-
webSearch: webSearchResults,
|
| 291 |
-
updates,
|
| 292 |
-
id: crypto.randomUUID(),
|
| 293 |
-
createdAt: new Date(),
|
| 294 |
-
updatedAt: new Date(),
|
| 295 |
-
},
|
| 296 |
-
];
|
| 297 |
-
} else {
|
| 298 |
-
// abort check
|
| 299 |
-
const date = abortedGenerations.get(convId.toString());
|
| 300 |
-
if (date && date > promptedAt) {
|
| 301 |
-
break;
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
if (!output) {
|
| 305 |
-
break;
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
// otherwise we just concatenate tokens
|
| 309 |
-
lastMessage.content += output.token.text;
|
| 310 |
}
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
} else {
|
| 313 |
-
|
| 314 |
// add output.generated text to the last message
|
| 315 |
// strip end tokens from the output.generated_text
|
| 316 |
const text = (model.parameters.stop ?? []).reduce((acc: string, curr: string) => {
|
| 317 |
if (acc.endsWith(curr)) {
|
| 318 |
-
interrupted = false;
|
| 319 |
return acc.slice(0, acc.length - curr.length);
|
| 320 |
}
|
| 321 |
return acc;
|
| 322 |
}, output.generated_text.trimEnd());
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
{
|
| 327 |
-
...messages[messages.length - 1],
|
| 328 |
-
content: previousContent + text,
|
| 329 |
-
interrupted, // if its a special token it finished on its own, else it was interrupted
|
| 330 |
-
updates,
|
| 331 |
-
updatedAt: new Date(),
|
| 332 |
-
},
|
| 333 |
-
];
|
| 334 |
}
|
| 335 |
}
|
| 336 |
} catch (e) {
|
|
@@ -343,7 +385,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 343 |
},
|
| 344 |
{
|
| 345 |
$set: {
|
| 346 |
-
messages,
|
| 347 |
title: conv?.title,
|
| 348 |
updatedAt: new Date(),
|
| 349 |
},
|
|
@@ -355,7 +397,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 355 |
|
| 356 |
update({
|
| 357 |
type: "finalAnswer",
|
| 358 |
-
text:
|
| 359 |
});
|
| 360 |
|
| 361 |
await summarizeIfNeeded;
|
|
@@ -370,7 +412,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
| 370 |
},
|
| 371 |
{
|
| 372 |
$set: {
|
| 373 |
-
messages,
|
| 374 |
title: conv.title,
|
| 375 |
updatedAt: new Date(),
|
| 376 |
},
|
|
|
|
| 9 |
import { z } from "zod";
|
| 10 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
| 11 |
import { runWebSearch } from "$lib/server/websearch/runWebSearch";
|
|
|
|
| 12 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
| 13 |
import { summarize } from "$lib/server/summarize";
|
| 14 |
import { uploadFile } from "$lib/server/files/uploadFile";
|
| 15 |
import sizeof from "image-size";
|
| 16 |
+
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
|
| 17 |
+
import { isMessageId } from "$lib/utils/tree/isMessageId";
|
| 18 |
+
import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
|
| 19 |
+
import { addChildren } from "$lib/utils/tree/addChildren.js";
|
| 20 |
+
import { addSibling } from "$lib/utils/tree/addSibling.js";
|
| 21 |
+
import { preprocessMessages } from "$lib/server/preprocessMessages.js";
|
| 22 |
|
| 23 |
export async function POST({ request, locals, params, getClientAddress }) {
|
| 24 |
const id = z.string().parse(params.id);
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
// check if the user has access to the conversation
|
| 36 |
+
const convBeforeCheck = await collections.conversations.findOne({
|
| 37 |
+
_id: convId,
|
| 38 |
+
...authCondition(locals),
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
if (convBeforeCheck && !convBeforeCheck.rootMessageId) {
|
| 42 |
+
const res = await collections.conversations.updateOne(
|
| 43 |
+
{
|
| 44 |
+
_id: convId,
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
$set: {
|
| 48 |
+
...convBeforeCheck,
|
| 49 |
+
...convertLegacyConversation(convBeforeCheck),
|
| 50 |
+
},
|
| 51 |
+
}
|
| 52 |
+
);
|
| 53 |
+
|
| 54 |
+
if (!res.acknowledged) {
|
| 55 |
+
throw error(500, "Failed to convert conversation");
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
const conv = await collections.conversations.findOne({
|
| 60 |
_id: convId,
|
| 61 |
...authCondition(locals),
|
|
|
|
| 125 |
files: b64files,
|
| 126 |
} = z
|
| 127 |
.object({
|
| 128 |
+
id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
|
| 129 |
inputs: z.optional(z.string().trim().min(1)),
|
|
|
|
| 130 |
is_retry: z.optional(z.boolean()),
|
| 131 |
is_continue: z.optional(z.boolean()),
|
| 132 |
web_search: z.optional(z.boolean()),
|
|
|
|
| 166 |
hashes = await Promise.all(files.map(async (file) => await uploadFile(file, conv)));
|
| 167 |
}
|
| 168 |
|
| 169 |
+
// we will append tokens to the content of this message
|
| 170 |
+
let messageToWriteToId: Message["id"] | undefined = undefined;
|
| 171 |
+
// used for building the prompt, subtree of the conversation that goes from the latest message to the root
|
| 172 |
+
let messagesForPrompt: Message[] = [];
|
| 173 |
|
| 174 |
+
if (isContinue && messageId) {
|
| 175 |
+
// if it's the last message and we continue then we build the prompt up to the last message
|
| 176 |
+
// we will strip the end tokens afterwards when the prompt is built
|
| 177 |
+
if ((conv.messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) {
|
| 178 |
+
throw error(400, "Can only continue the last message");
|
| 179 |
+
}
|
| 180 |
+
messageToWriteToId = messageId;
|
| 181 |
+
messagesForPrompt = buildSubtree(conv, messageId);
|
| 182 |
+
} else if (isRetry && messageId) {
|
| 183 |
+
// two cases, if we're retrying a user message with a newPrompt set,
|
| 184 |
+
// it means we're editing a user message
|
| 185 |
+
// if we're retrying on an assistant message, newPrompt cannot be set
|
| 186 |
+
// it means we're retrying the last assistant message for a new answer
|
| 187 |
+
|
| 188 |
+
const messageToRetry = conv.messages.find((message) => message.id === messageId);
|
| 189 |
+
|
| 190 |
+
if (!messageToRetry) {
|
| 191 |
+
throw error(404, "Message not found");
|
| 192 |
+
}
|
| 193 |
|
| 194 |
+
if (messageToRetry.from === "user" && newPrompt) {
|
| 195 |
+
// add a sibling to this message from the user, with the alternative prompt
|
| 196 |
+
// add a children to that sibling, where we can write to
|
| 197 |
+
const newUserMessageId = addSibling(conv, { from: "user", content: newPrompt }, messageId);
|
| 198 |
+
messageToWriteToId = addChildren(
|
| 199 |
+
conv,
|
| 200 |
+
{ from: "assistant", content: "", files: hashes },
|
| 201 |
+
newUserMessageId
|
| 202 |
+
);
|
| 203 |
+
messagesForPrompt = buildSubtree(conv, newUserMessageId);
|
| 204 |
+
} else if (messageToRetry.from === "assistant") {
|
| 205 |
+
// we're retrying an assistant message, to generate a new answer
|
| 206 |
+
// just add a sibling to the assistant answer where we can write to
|
| 207 |
+
messageToWriteToId = addSibling(conv, { from: "assistant", content: "" }, messageId);
|
| 208 |
+
messagesForPrompt = buildSubtree(conv, messageId);
|
| 209 |
+
messagesForPrompt.pop(); // don't need the latest assistant message in the prompt since we're retrying it
|
| 210 |
+
}
|
| 211 |
+
} else {
|
| 212 |
+
// just a normal linear conversation, so we add the user message
|
| 213 |
+
// and the blank assistant message back to back
|
| 214 |
+
const newUserMessageId = addChildren(
|
| 215 |
+
conv,
|
| 216 |
+
{
|
| 217 |
+
from: "user",
|
| 218 |
+
content: newPrompt ?? "",
|
| 219 |
+
files: hashes,
|
| 220 |
+
createdAt: new Date(),
|
| 221 |
+
updatedAt: new Date(),
|
| 222 |
+
},
|
| 223 |
+
messageId
|
| 224 |
+
);
|
| 225 |
|
| 226 |
+
messageToWriteToId = addChildren(
|
| 227 |
+
conv,
|
| 228 |
+
{
|
| 229 |
+
from: "assistant",
|
| 230 |
+
content: "",
|
| 231 |
+
createdAt: new Date(),
|
| 232 |
+
updatedAt: new Date(),
|
| 233 |
+
},
|
| 234 |
+
newUserMessageId
|
| 235 |
+
);
|
| 236 |
+
// build the prompt from the user message
|
| 237 |
+
messagesForPrompt = buildSubtree(conv, newUserMessageId);
|
| 238 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
const messageToWriteTo = conv.messages.find((message) => message.id === messageToWriteToId);
|
| 241 |
+
if (!messageToWriteTo) {
|
| 242 |
+
throw error(500, "Failed to create message");
|
| 243 |
+
}
|
| 244 |
+
if (messagesForPrompt.length === 0) {
|
| 245 |
+
throw error(500, "Failed to create prompt");
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// update the conversation with the new messages
|
| 249 |
await collections.conversations.updateOne(
|
| 250 |
{
|
| 251 |
_id: convId,
|
| 252 |
},
|
| 253 |
{
|
| 254 |
$set: {
|
| 255 |
+
messages: conv.messages,
|
| 256 |
title: conv.title,
|
| 257 |
updatedAt: new Date(),
|
| 258 |
},
|
|
|
|
| 264 |
// we now build the stream
|
| 265 |
const stream = new ReadableStream({
|
| 266 |
async start(controller) {
|
| 267 |
+
messageToWriteTo.updates ??= [];
|
|
|
|
|
|
|
|
|
|
| 268 |
function update(newUpdate: MessageUpdate) {
|
| 269 |
if (newUpdate.type !== "stream") {
|
| 270 |
+
messageToWriteTo?.updates?.push(newUpdate);
|
| 271 |
}
|
| 272 |
|
| 273 |
if (newUpdate.type === "stream" && newUpdate.token === "") {
|
|
|
|
| 284 |
update({ type: "status", status: "started" });
|
| 285 |
|
| 286 |
const summarizeIfNeeded = (async () => {
|
| 287 |
+
if (conv.title === "New Chat" && conv.messages.length === 3) {
|
| 288 |
try {
|
| 289 |
+
conv.title = (await summarize(conv.messages[1].content)) ?? conv.title;
|
| 290 |
update({ type: "status", status: "title", message: conv.title });
|
| 291 |
+
await collections.conversations.updateOne(
|
| 292 |
+
{
|
| 293 |
+
_id: convId,
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
$set: {
|
| 297 |
+
title: conv?.title,
|
| 298 |
+
updatedAt: new Date(),
|
| 299 |
+
},
|
| 300 |
+
}
|
| 301 |
+
);
|
| 302 |
} catch (e) {
|
| 303 |
console.error(e);
|
| 304 |
}
|
|
|
|
| 311 |
},
|
| 312 |
{
|
| 313 |
$set: {
|
|
|
|
| 314 |
title: conv.title,
|
| 315 |
updatedAt: new Date(),
|
| 316 |
},
|
| 317 |
}
|
| 318 |
);
|
| 319 |
|
| 320 |
+
// perform websearch if needed
|
|
|
|
| 321 |
if (webSearch && !isContinue && !conv.assistantId) {
|
| 322 |
+
messageToWriteTo.webSearch = await runWebSearch(conv, messagesForPrompt, update);
|
|
|
|
|
|
|
|
|
|
| 323 |
}
|
| 324 |
|
| 325 |
+
// inject websearch result & optionally images into the messages
|
| 326 |
+
const processedMessages = await preprocessMessages(
|
| 327 |
+
messagesForPrompt,
|
| 328 |
+
model.multimodal,
|
| 329 |
+
convId
|
| 330 |
+
);
|
| 331 |
|
| 332 |
+
const previousText = messageToWriteTo.content;
|
|
|
|
|
|
|
| 333 |
|
| 334 |
try {
|
| 335 |
const endpoint = await model.getEndpoint();
|
| 336 |
+
for await (const output of await endpoint({
|
| 337 |
+
messages: processedMessages,
|
| 338 |
+
preprompt: conv.preprompt,
|
| 339 |
+
continueMessage: isContinue,
|
| 340 |
+
})) {
|
| 341 |
// if not generated_text is here it means the generation is not done
|
| 342 |
if (!output.generated_text) {
|
| 343 |
// else we get the next token
|
|
|
|
| 346 |
type: "stream",
|
| 347 |
token: output.token.text,
|
| 348 |
});
|
| 349 |
+
// abort check
|
| 350 |
+
const date = abortedGenerations.get(convId.toString());
|
| 351 |
+
if (date && date > promptedAt) {
|
| 352 |
+
break;
|
| 353 |
+
}
|
| 354 |
+
// no output check
|
| 355 |
+
if (!output) {
|
| 356 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
+
|
| 359 |
+
// otherwise we just concatenate tokens
|
| 360 |
+
messageToWriteTo.content += output.token.text;
|
| 361 |
}
|
| 362 |
} else {
|
| 363 |
+
messageToWriteTo.interrupted = !output.token.special;
|
| 364 |
// add output.generated text to the last message
|
| 365 |
// strip end tokens from the output.generated_text
|
| 366 |
const text = (model.parameters.stop ?? []).reduce((acc: string, curr: string) => {
|
| 367 |
if (acc.endsWith(curr)) {
|
| 368 |
+
messageToWriteTo.interrupted = false;
|
| 369 |
return acc.slice(0, acc.length - curr.length);
|
| 370 |
}
|
| 371 |
return acc;
|
| 372 |
}, output.generated_text.trimEnd());
|
| 373 |
|
| 374 |
+
messageToWriteTo.content = previousText + text;
|
| 375 |
+
messageToWriteTo.updatedAt = new Date();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
}
|
| 377 |
}
|
| 378 |
} catch (e) {
|
|
|
|
| 385 |
},
|
| 386 |
{
|
| 387 |
$set: {
|
| 388 |
+
messages: conv.messages,
|
| 389 |
title: conv?.title,
|
| 390 |
updatedAt: new Date(),
|
| 391 |
},
|
|
|
|
| 397 |
|
| 398 |
update({
|
| 399 |
type: "finalAnswer",
|
| 400 |
+
text: messageToWriteTo.content,
|
| 401 |
});
|
| 402 |
|
| 403 |
await summarizeIfNeeded;
|
|
|
|
| 412 |
},
|
| 413 |
{
|
| 414 |
$set: {
|
| 415 |
+
messages: conv.messages,
|
| 416 |
title: conv.title,
|
| 417 |
updatedAt: new Date(),
|
| 418 |
},
|
|
@@ -2,6 +2,8 @@ import { buildPrompt } from "$lib/buildPrompt";
|
|
| 2 |
import { authCondition } from "$lib/server/auth";
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import { models } from "$lib/server/models";
|
|
|
|
|
|
|
| 5 |
import { error } from "@sveltejs/kit";
|
| 6 |
import { ObjectId } from "mongodb";
|
| 7 |
|
|
@@ -24,7 +26,7 @@ export async function GET({ params, locals }) {
|
|
| 24 |
|
| 25 |
const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId);
|
| 26 |
|
| 27 |
-
if (messageIndex === -1) {
|
| 28 |
throw error(404, "Message not found");
|
| 29 |
}
|
| 30 |
|
|
@@ -34,11 +36,10 @@ export async function GET({ params, locals }) {
|
|
| 34 |
throw error(404, "Conversation model not found");
|
| 35 |
}
|
| 36 |
|
| 37 |
-
const messagesUpTo = conv
|
| 38 |
|
| 39 |
const prompt = await buildPrompt({
|
| 40 |
preprompt: conv.preprompt,
|
| 41 |
-
webSearch: messagesUpTo[messagesUpTo.length - 1].webSearch,
|
| 42 |
messages: messagesUpTo,
|
| 43 |
model,
|
| 44 |
});
|
|
|
|
| 2 |
import { authCondition } from "$lib/server/auth";
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import { models } from "$lib/server/models";
|
| 5 |
+
import { buildSubtree } from "$lib/utils/tree/buildSubtree";
|
| 6 |
+
import { isMessageId } from "$lib/utils/tree/isMessageId";
|
| 7 |
import { error } from "@sveltejs/kit";
|
| 8 |
import { ObjectId } from "mongodb";
|
| 9 |
|
|
|
|
| 26 |
|
| 27 |
const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId);
|
| 28 |
|
| 29 |
+
if (!isMessageId(messageId) || messageIndex === -1) {
|
| 30 |
throw error(404, "Message not found");
|
| 31 |
}
|
| 32 |
|
|
|
|
| 36 |
throw error(404, "Conversation model not found");
|
| 37 |
}
|
| 38 |
|
| 39 |
+
const messagesUpTo = buildSubtree(conv, messageId);
|
| 40 |
|
| 41 |
const prompt = await buildPrompt({
|
| 42 |
preprompt: conv.preprompt,
|
|
|
|
| 43 |
messages: messagesUpTo,
|
| 44 |
model,
|
| 45 |
});
|
|
@@ -32,10 +32,11 @@ export async function POST({ params, url, locals }) {
|
|
| 32 |
|
| 33 |
const shared: SharedConversation = {
|
| 34 |
_id: nanoid(7),
|
| 35 |
-
createdAt: new Date(),
|
| 36 |
-
messages: conversation.messages,
|
| 37 |
hash,
|
|
|
|
| 38 |
updatedAt: new Date(),
|
|
|
|
|
|
|
| 39 |
title: conversation.title,
|
| 40 |
model: conversation.model,
|
| 41 |
embeddingModel: conversation.embeddingModel,
|
|
|
|
| 32 |
|
| 33 |
const shared: SharedConversation = {
|
| 34 |
_id: nanoid(7),
|
|
|
|
|
|
|
| 35 |
hash,
|
| 36 |
+
createdAt: new Date(),
|
| 37 |
updatedAt: new Date(),
|
| 38 |
+
rootMessageId: conversation.rootMessageId,
|
| 39 |
+
messages: conversation.messages,
|
| 40 |
title: conversation.title,
|
| 41 |
model: conversation.model,
|
| 42 |
embeddingModel: conversation.embeddingModel,
|