123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- <script lang="ts">
- export let show = false;
- export let selectedModelId = '';
- import { marked } from 'marked';
- import { toast } from 'svelte-sonner';
- import { goto } from '$app/navigation';
- import { onMount, tick, getContext } from 'svelte';
- import {
- OLLAMA_API_BASE_URL,
- OPENAI_API_BASE_URL,
- WEBUI_API_BASE_URL,
- WEBUI_BASE_URL
- } from '$lib/constants';
- import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
- import { chatCompletion, generateOpenAIChatCompletion } from '$lib/apis/openai';
- import { splitStream } from '$lib/utils';
- import Messages from '$lib/components/notes/NoteEditor/Chat/Messages.svelte';
- import MessageInput from '$lib/components/channel/MessageInput.svelte';
- import XMark from '$lib/components/icons/XMark.svelte';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import Pencil from '$lib/components/icons/Pencil.svelte';
- import PencilSquare from '$lib/components/icons/PencilSquare.svelte';
- const i18n = getContext('i18n');
- export let enhancing = false;
- export let streaming = false;
- export let stopResponseFlag = false;
- export let note = null;
- export let files = [];
- export let messages = [];
- export let onInsert = (content) => {};
- export let onStop = () => {};
- export let scrollToBottomHandler = () => {};
- let loaded = false;
- let loading = false;
- let messagesContainerElement: HTMLDivElement;
- let system = '';
- let editorEnabled = false;
- let chatInputElement = null;
- const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
- ## Task
- Based on the user's instruction, update and enhance the existing notes by incorporating relevant and accurate information from the provided context. Ensure all edits strictly follow the user’s intent.
- ## Input Structure
- - Existing notes: Enclosed within <notes></notes> XML tags.
- - Additional context: Enclosed within <context></context> XML tags.
- - Editing instruction: Provided in the user message.
- ## Output Instructions
- - Deliver a single, rewritten version of the notes in markdown format.
- - Integrate information from the context only if it directly supports the user's instruction.
- - Use clear, organized markdown elements: headings, bullet points, numbered lists, bold and italic text as appropriate.
- - Focus on improving clarity, completeness, and usefulness of the notes.
- - Return only the final, fully-edited markdown notes—do not include explanations, reasoning, or XML tags.
- `;
- let scrolledToBottom = true;
- const scrollToBottom = () => {
- if (messagesContainerElement) {
- if (scrolledToBottom) {
- messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
- }
- }
- };
- const onScroll = () => {
- if (messagesContainerElement) {
- scrolledToBottom =
- messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
- messagesContainerElement.clientHeight + 10;
- }
- };
- const chatCompletionHandler = async () => {
- if (selectedModelId === '') {
- toast.error($i18n.t('Please select a model.'));
- return;
- }
- const model = $models.find((model) => model.id === selectedModelId);
- if (!model) {
- selectedModelId = '';
- return;
- }
- let responseMessage;
- if (messages.at(-1)?.role === 'assistant') {
- responseMessage = messages.at(-1);
- } else {
- responseMessage = {
- role: 'assistant',
- content: '',
- done: false
- };
- messages.push(responseMessage);
- messages = messages;
- }
- await tick();
- scrollToBottom();
- let enhancedContent = {
- json: null,
- html: '',
- md: ''
- };
- system = '';
- if (editorEnabled) {
- system = `${DEFAULT_DOCUMENT_EDITOR_PROMPT}\n\n`;
- } else {
- system = `You are a helpful assistant. Please answer the user's questions based on the context provided.\n\n`;
- }
- system +=
- `<notes>${note?.data?.content?.md ?? ''}</notes>` +
- (files && files.length > 0
- ? `\n<context>${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}</context>`
- : '');
- const chatMessages = JSON.parse(
- JSON.stringify([
- {
- role: 'system',
- content: `${system}`
- },
- ...messages
- ])
- );
- const [res, controller] = await chatCompletion(
- localStorage.token,
- {
- model: model.id,
- stream: true,
- messages: chatMessages
- // ...(files && files.length > 0 ? { files } : {}) // TODO: Decide whether to use native file handling or not
- },
- `${WEBUI_BASE_URL}/api`
- );
- await tick();
- scrollToBottom();
- let messageContent = '';
- if (res && res.ok) {
- const reader = res.body
- .pipeThrough(new TextDecoderStream())
- .pipeThrough(splitStream('\n'))
- .getReader();
- while (true) {
- const { value, done } = await reader.read();
- if (done || stopResponseFlag) {
- if (stopResponseFlag) {
- controller.abort('User: Stop Response');
- }
- if (editorEnabled) {
- enhancing = false;
- streaming = false;
- }
- break;
- }
- try {
- let lines = value.split('\n');
- for (const line of lines) {
- if (line !== '') {
- console.log(line);
- if (line === 'data: [DONE]') {
- if (editorEnabled) {
- responseMessage.content = '<status title="Edited" done="true" />';
- }
- responseMessage.done = true;
- messages = messages;
- } else {
- let data = JSON.parse(line.replace(/^data: /, ''));
- console.log(data);
- let deltaContent = data.choices[0]?.delta?.content ?? '';
- if (responseMessage.content == '' && deltaContent == '\n') {
- continue;
- } else {
- if (editorEnabled) {
- enhancing = true;
- streaming = true;
- enhancedContent.md += deltaContent;
- enhancedContent.html = marked.parse(enhancedContent.md);
- note.data.content.md = enhancedContent.md;
- note.data.content.html = enhancedContent.html;
- note.data.content.json = null;
- responseMessage.content = '<status title="Editing" done="false" />';
- scrollToBottomHandler();
- messages = messages;
- } else {
- messageContent += deltaContent;
- responseMessage.content = messageContent;
- messages = messages;
- }
- await tick();
- }
- }
- }
- }
- } catch (error) {
- console.log(error);
- }
- scrollToBottom();
- }
- }
- };
- const submitHandler = async (e) => {
- const { content, data } = e;
- if (selectedModelId && content) {
- messages.push({
- role: 'user',
- content: content
- });
- messages = messages;
- await tick();
- scrollToBottom();
- loading = true;
- await chatCompletionHandler();
- messages = messages.map((message) => {
- message.done = true;
- return message;
- });
- loading = false;
- stopResponseFlag = false;
- }
- };
- onMount(async () => {
- if ($user?.role !== 'admin') {
- await goto('/');
- }
- if ($settings?.models) {
- selectedModelId = $settings?.models[0];
- } else if ($config?.default_models) {
- selectedModelId = $config?.default_models.split(',')[0];
- } else {
- selectedModelId = '';
- }
- loaded = true;
- scrollToBottom();
- });
- </script>
- <div class="flex items-center mb-2 pt-1">
- <div class=" -translate-x-1.5 flex items-center">
- <button
- class="p-0.5 bg-transparent transition rounded-lg"
- on:click={() => {
- show = !show;
- }}
- >
- <XMark className="size-5" strokeWidth="2.5" />
- </button>
- </div>
- <div class=" font-medium text-base flex items-center gap-1">
- <div>
- {$i18n.t('Chat')}
- </div>
- <div>
- <Tooltip
- content={$i18n.t(
- 'This feature is experimental and may be modified or discontinued without notice.'
- )}
- position="top"
- className="inline-block"
- >
- <span class="text-gray-500 text-sm">({$i18n.t('Experimental')})</span>
- </Tooltip>
- </div>
- </div>
- </div>
- <div class="flex flex-col items-center mb-2 flex-1 @container">
- <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
- <div class="mx-auto w-full md:px-0 h-full relative">
- <div class=" flex flex-col h-full">
- <div
- class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
- id="messages-container"
- bind:this={messagesContainerElement}
- on:scroll={onScroll}
- >
- <div class=" h-full w-full flex flex-col">
- <div class="flex-1 p-1">
- <Messages bind:messages {onInsert} />
- </div>
- </div>
- </div>
- <div class=" pb-2">
- <MessageInput
- bind:chatInputElement
- acceptFiles={false}
- inputLoading={loading}
- onSubmit={submitHandler}
- {onStop}
- >
- <div slot="menu" class="flex items-center justify-between gap-2 w-full pr-2">
- <div>
- <Tooltip content={$i18n.t('Edit')} placement="top">
- <button
- on:click|preventDefault={() => (editorEnabled = !editorEnabled)}
- type="button"
- class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {editorEnabled
- ? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
- : 'bg-transparent text-gray-600 dark:text-gray-300 '}"
- >
- <PencilSquare className="size-4" strokeWidth="1.75" />
- <span
- class="block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
- >{$i18n.t('Edit')}</span
- >
- </button>
- </Tooltip>
- </div>
- <Tooltip content={selectedModelId}>
- <select
- class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-20"
- bind:value={selectedModelId}
- >
- {#each $models as model}
- <option value={model.id} class="bg-gray-50 dark:bg-gray-700"
- >{model.name}</option
- >
- {/each}
- </select>
- </Tooltip>
- </div>
- </MessageInput>
- </div>
- </div>
- </div>
- </div>
- </div>
|