Spaces:
Runtime error
Runtime error
<script lang="ts"> | |
import ChatWindow from "$lib/components/chat/ChatWindow.svelte"; | |
import { pendingMessage } from "$lib/stores/pendingMessage"; | |
import { pendingMessageIdToRetry } from "$lib/stores/pendingMessageIdToRetry"; | |
import { onMount } from "svelte"; | |
import { page } from "$app/stores"; | |
import { invalidate } from "$app/navigation"; | |
import { base } from "$app/paths"; | |
import { shareConversation } from "$lib/shareConversation"; | |
import { UrlDependency } from "$lib/types/UrlDependency"; | |
import { ERROR_MESSAGES, error } from "$lib/stores/errors"; | |
import { randomUUID } from "$lib/utils/randomUuid"; | |
import { findCurrentModel } from "$lib/utils/models"; | |
import { webSearchParameters } from "$lib/stores/webSearchParameters"; | |
import type { WebSearchMessage } from "$lib/types/WebSearch"; | |
import type { Message } from "$lib/types/Message"; | |
import { PUBLIC_APP_DISCLAIMER } from "$env/static/public"; | |
import { pipeline, Pipeline, env as env_transformers } from "@xenova/transformers"; | |
import { isloading_writable, curr_model_writable, is_init_writable, cancel_writable } from "../../LayoutWritable.js"; | |
import { map_writable, phi_writable } from "$lib/components/LoadingModalWritable.js"; | |
import { params_writable } from "./ParamsWritable.js"; | |
import { addMessageToChat, getChats, getMessages, getTitle, getModel } from "../../LocalDB.js"; | |
import { env } from "$env/dynamic/public"; | |
export let data; | |
let curr_model_id = 0; | |
curr_model_writable.subscribe((val) => { | |
curr_model_id = val; | |
}); | |
let pipelineWorker; | |
let pipe: Pipeline; | |
let id = ""; | |
let title_ret = "BlindChat"; | |
let curr_model = data.model; | |
let curr_model_obj; | |
let id_now; | |
let messages = []; | |
let lastLoadedMessages = []; | |
let isAborted = false; | |
let webSearchMessages: WebSearchMessage[] = []; | |
let loading = false; | |
let pending = false; | |
let loginRequired = false; | |
//The code consider that the datalayer variable does not exist | |
//but it is instantiated by Google Tag Manager during runtime | |
// Create a callback function for messages from the worker thread. | |
const onMessageReceived = (e) => { | |
let lastMessage: any = undefined; | |
switch (e.data.status) { | |
case "initiate": | |
if (e.data.file == "tokenizer.json") // Avoid to send the tag multiple times | |
dataLayer.push({'event': 'debut_chargement_chat', 'nom_modele':[e.data.name]}); | |
break; | |
case "progress": | |
isloading_writable.set(true); | |
if (e.data.no_progress_bar == undefined || e.data.no_progress_bar == false) { | |
map_writable.set([e.data.file, e.data.progress]); | |
} | |
else { | |
map_writable.set(["phi", "-1"]) | |
} | |
break; | |
case "init_model": | |
break; | |
case "done": | |
break; | |
case "ready": | |
dataLayer.push({'event': 'fin_chargement_chat', 'nom_modele':[e.data.name]}); | |
isloading_writable.set(false); | |
is_init_writable.set(false); | |
phi_writable.set(false); | |
break; | |
case "update": | |
if (e.data.id_now == id_now) { | |
if (lastMessage == undefined) lastMessage = messages[messages.length - 1]; | |
lastMessage.content = e.data.output; | |
lastMessage.webSearchId = e.data.searchID; | |
lastMessage.updatedAt = new Date(); | |
messages = [...messages]; | |
} | |
else { | |
pipelineWorker.postMessage({ command: "abort" }) | |
} | |
break; | |
case "aborted": | |
case "complete": | |
if (e.data.id_now == id_now) { | |
dataLayer.push({'event': 'reponse_message'}); | |
lastMessage = messages[messages.length - 1]; | |
lastMessage.webSearchId = e.data.searchID; | |
lastMessage.updatedAt = new Date(); | |
addMessageToChat($page.params.id, lastMessage); | |
messages = [...messages]; | |
lastMessage = undefined; | |
loading = false; | |
pending = false; | |
webSearchMessages = []; | |
if (messages.filter((m) => m.from === "user").length === 1) { | |
invalidate(UrlDependency.ConversationList).catch(console.error); | |
} else { | |
invalidate(UrlDependency.ConversationList).then((value) => {}); | |
} | |
} | |
break; | |
} | |
}; | |
async function getTextGenerationStream( | |
inputs: string, | |
messageId: string, | |
isRetry = false, | |
webSearchId?: string | |
) { | |
let conversationId = $page.params.id; | |
const responseId = randomUUID(); | |
let opt = ""; | |
messages = [ | |
...messages, | |
// id doesn't match the backend id but it's not important for assistant messages | |
{ | |
from: "assistant", | |
content: "", | |
id: responseId, | |
createdAt: new Date(), | |
updatedAt: new Date(), | |
isCode: curr_model_obj.is_code ?? false, | |
}, | |
]; | |
let msg = { | |
content: inputs, | |
from: "user", | |
id: randomUUID(), | |
createdAt: new Date(), | |
updatedAt: new Date(), | |
isCode: false, | |
}; | |
dataLayer.push({'event': 'envoi_message'}); | |
addMessageToChat(conversationId, msg, curr_model); | |
let lastMessage = messages[messages.length - 1]; | |
pipelineWorker.postMessage({ | |
is_phi: curr_model_obj.is_phi ?? false, | |
id_now: id_now, | |
task: curr_model_obj.type, | |
max_new_tokens: curr_model_obj.parameters?.max_new_tokens ?? 256, | |
temperature: curr_model_obj.parameters?.temperature ?? 0.7, | |
model: curr_model, | |
text: inputs, | |
webSearchId: webSearchId, | |
conversationId: conversationId, | |
}); | |
} | |
async function summarizeTitle(id: string) { | |
await fetch(`${base}/conversation/${id}/summarize`, { | |
method: "POST", | |
}); | |
} | |
async function writeMessage(message: string, messageId = randomUUID()) { | |
if (!message.trim()) return; | |
try { | |
isAborted = false; | |
loading = true; | |
pending = true; | |
let retryMessageIndex = messages.findIndex((msg) => msg.id === messageId); | |
const isRetry = retryMessageIndex !== -1; | |
if (!isRetry) { | |
retryMessageIndex = messages.length; | |
} | |
messages = [ | |
...messages.slice(0, retryMessageIndex), | |
{ from: "user", content: message, id: messageId }, | |
]; | |
let searchResponseId: string | null = ""; | |
if ($webSearchParameters.useSearch) { | |
webSearchMessages = []; | |
const res = await fetch( | |
`${base}/conversation/${$page.params.id}/web-search?` + | |
new URLSearchParams({ prompt: message }), | |
{ | |
method: "GET", | |
} | |
); | |
// required bc linting doesn't see TextDecoderStream for some reason? | |
// eslint-disable-next-line no-undef | |
const encoder = new TextDecoderStream(); | |
const reader = res?.body?.pipeThrough(encoder).getReader(); | |
while (searchResponseId === "") { | |
await new Promise((r) => setTimeout(r, 25)); | |
if (isAborted) { | |
reader?.cancel(); | |
return; | |
} | |
reader | |
?.read() | |
.then(async ({ done, value }) => { | |
if (done) { | |
reader.cancel(); | |
return; | |
} | |
try { | |
webSearchMessages = (JSON.parse(value) as { messages: WebSearchMessage[] }) | |
.messages; | |
} catch (parseError) { | |
// in case of parsing error we wait for the next message | |
return; | |
} | |
const lastSearchMessage = webSearchMessages[webSearchMessages.length - 1]; | |
if (lastSearchMessage.type === "result") { | |
searchResponseId = lastSearchMessage.id; | |
reader.cancel(); | |
return; | |
} | |
}) | |
.catch(() => { | |
searchResponseId = null; | |
}); | |
} | |
} | |
await getTextGenerationStream(message, messageId, isRetry, searchResponseId ?? undefined); | |
} catch (err) { | |
if (err instanceof Error && err.message.includes("overloaded")) { | |
$error = "Too much traffic, please try again."; | |
} else if (err instanceof Error && err.message.includes("429")) { | |
$error = ERROR_MESSAGES.rateLimited; | |
} else if (err instanceof Error) { | |
$error = err.message; | |
} else { | |
$error = ERROR_MESSAGES.default; | |
} | |
console.error(err); | |
} finally { | |
loading = curr_model_obj.is_phi ?? false; | |
pending = false; | |
} | |
} | |
async function voteMessage(score: Message["score"], messageId: string) { | |
let conversationId = $page.params.id; | |
let oldScore: Message["score"] | undefined; | |
// optimistic update to avoid waiting for the server | |
messages = messages.map((message) => { | |
if (message.id === messageId) { | |
oldScore = message.score; | |
return { ...message, score: score }; | |
} | |
return message; | |
}); | |
try { | |
await fetch(`${base}/conversation/${conversationId}/message/${messageId}/vote`, { | |
method: "POST", | |
body: JSON.stringify({ score }), | |
}); | |
} catch { | |
// revert score on any error | |
messages = messages.map((message) => { | |
return message.id !== messageId ? message : { ...message, score: oldScore }; | |
}); | |
} | |
} | |
params_writable.subscribe(async (value) => { | |
if (value != id) { | |
id = value; | |
//title_ret = await getTitle(value) | |
let res = await getMessages(value); | |
curr_model = await getModel(value); | |
if (curr_model === undefined || curr_model.length == 0) { | |
curr_model_obj = findCurrentModel( | |
[...data.models, ...data.oldModels], | |
data.models[curr_model_id].name | |
); | |
curr_model = curr_model_obj.name; | |
} else { | |
curr_model_obj = findCurrentModel([...data.models, ...data.oldModels], curr_model); | |
} | |
if (res != undefined) { | |
messages = res; | |
lastLoadedMessages = res; | |
} | |
id_now = randomUUID(); | |
} | |
}); | |
onMount(async () => { | |
curr_model = await getModel($page.params.id); | |
if (curr_model === undefined || curr_model.length == 0) { | |
curr_model_obj = findCurrentModel( | |
[...data.models, ...data.oldModels], | |
data.models[curr_model_id].name | |
); | |
curr_model = curr_model_obj.name; | |
} else { | |
curr_model_obj = findCurrentModel([...data.models, ...data.oldModels], curr_model); | |
} | |
id_now = randomUUID(); | |
const Worker = await import("./worker.js?worker"); | |
pipelineWorker = new Worker.default(); | |
//title_ret = await getTitle($page.params.id) | |
let res = await getMessages($page.params.id); | |
id = $page.params.id; | |
if (res != undefined) { | |
messages = res; | |
lastLoadedMessages = res; | |
} | |
pipelineWorker.addEventListener("message", onMessageReceived); | |
if ($pendingMessage) { | |
const val = $pendingMessage; | |
const messageId = $pendingMessageIdToRetry || undefined; | |
$pendingMessage = ""; | |
$pendingMessageIdToRetry = null; | |
writeMessage(val, messageId); | |
} | |
}); | |
$: $page.params.id, (isAborted = true); | |
$: title = title_ret; | |
$: loginRequired = | |
(data.requiresLogin | |
? !data.user | |
: !data.settings.ethicsModalAcceptedAt && !!PUBLIC_APP_DISCLAIMER) && | |
messages.length >= data.messagesBeforeLogin; | |
</script> | |
<svelte:head> | |
<title>{title}</title> | |
</svelte:head> | |
<ChatWindow | |
{loading} | |
{pending} | |
{messages} | |
bind:webSearchMessages | |
searches={{ ...data.searches }} | |
on:message={(event) => writeMessage(event.detail)} | |
on:retry={(event) => writeMessage(event.detail.content, event.detail.id)} | |
on:vote={(event) => voteMessage(event.detail.score, event.detail.id)} | |
on:share={() => shareConversation($page.params.id, data.title)} | |
on:stop={() => (isAborted = true, pipelineWorker.postMessage({ command: "abort" }))} | |
models={data.models} | |
currentModel={findCurrentModel([...data.models, ...data.oldModels], curr_model)} | |
settings={data.settings} | |
{loginRequired} | |
/> | |