|
@@ -2,6 +2,7 @@
|
|
export let show = false;
|
|
export let show = false;
|
|
export let selectedModelId = '';
|
|
export let selectedModelId = '';
|
|
|
|
|
|
|
|
+ import { marked } from 'marked';
|
|
import { toast } from 'svelte-sonner';
|
|
import { toast } from 'svelte-sonner';
|
|
|
|
|
|
import { goto } from '$app/navigation';
|
|
import { goto } from '$app/navigation';
|
|
@@ -23,14 +24,21 @@
|
|
import MessageInput from '$lib/components/channel/MessageInput.svelte';
|
|
import MessageInput from '$lib/components/channel/MessageInput.svelte';
|
|
import XMark from '$lib/components/icons/XMark.svelte';
|
|
import XMark from '$lib/components/icons/XMark.svelte';
|
|
import Tooltip from '$lib/components/common/Tooltip.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');
|
|
const i18n = getContext('i18n');
|
|
|
|
|
|
|
|
+ export let enhancing = false;
|
|
|
|
+ export let streaming = false;
|
|
|
|
+
|
|
export let note = null;
|
|
export let note = null;
|
|
|
|
+
|
|
export let files = [];
|
|
export let files = [];
|
|
export let messages = [];
|
|
export let messages = [];
|
|
|
|
|
|
export let onInsert = (content) => {};
|
|
export let onInsert = (content) => {};
|
|
|
|
+ export let scrollToBottomHandler = () => {};
|
|
|
|
|
|
let loaded = false;
|
|
let loaded = false;
|
|
|
|
|
|
@@ -40,13 +48,43 @@
|
|
let messagesContainerElement: HTMLDivElement;
|
|
let messagesContainerElement: HTMLDivElement;
|
|
|
|
|
|
let system = '';
|
|
let system = '';
|
|
|
|
+ let editorEnabled = false;
|
|
|
|
+
|
|
let chatInputElement = null;
|
|
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 = () => {
|
|
const scrollToBottom = () => {
|
|
- const element = messagesContainerElement;
|
|
|
|
|
|
+ if (messagesContainerElement) {
|
|
|
|
+ if (scrolledToBottom) {
|
|
|
|
+ messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
|
|
- if (element) {
|
|
|
|
- element.scrollTop = element?.scrollHeight;
|
|
|
|
|
|
+ const onScroll = () => {
|
|
|
|
+ if (messagesContainerElement) {
|
|
|
|
+ scrolledToBottom =
|
|
|
|
+ messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
|
|
|
|
+ messagesContainerElement.clientHeight + 10;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
@@ -83,37 +121,43 @@
|
|
await tick();
|
|
await tick();
|
|
scrollToBottom();
|
|
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(
|
|
const [res, controller] = await chatCompletion(
|
|
localStorage.token,
|
|
localStorage.token,
|
|
{
|
|
{
|
|
model: model.id,
|
|
model: model.id,
|
|
stream: true,
|
|
stream: true,
|
|
- messages: [
|
|
|
|
- system
|
|
|
|
- ? {
|
|
|
|
- role: 'system',
|
|
|
|
- content: system
|
|
|
|
- }
|
|
|
|
- : undefined,
|
|
|
|
- ...messages
|
|
|
|
- ].filter((message) => message),
|
|
|
|
- files: [
|
|
|
|
- ...(note?.data?.content?.md
|
|
|
|
- ? [
|
|
|
|
- {
|
|
|
|
- id: `note:${note?.id ?? 'note'}`,
|
|
|
|
- name: note?.name ?? 'Note',
|
|
|
|
- file: {
|
|
|
|
- data: {
|
|
|
|
- content: note?.data?.content?.md
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
- context: 'full'
|
|
|
|
- }
|
|
|
|
- ]
|
|
|
|
- : []), // Include the note content as a file
|
|
|
|
- ...files
|
|
|
|
- ]
|
|
|
|
|
|
+ messages: chatMessages
|
|
|
|
+ // ...(files && files.length > 0 ? { files } : {}) // TODO: Decide whether to use native file handling or not
|
|
},
|
|
},
|
|
`${WEBUI_BASE_URL}/api`
|
|
`${WEBUI_BASE_URL}/api`
|
|
);
|
|
);
|
|
@@ -121,6 +165,8 @@
|
|
await tick();
|
|
await tick();
|
|
scrollToBottom();
|
|
scrollToBottom();
|
|
|
|
|
|
|
|
+ let messageContent = '';
|
|
|
|
+
|
|
if (res && res.ok) {
|
|
if (res && res.ok) {
|
|
const reader = res.body
|
|
const reader = res.body
|
|
.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(new TextDecoderStream())
|
|
@@ -133,6 +179,11 @@
|
|
if (stopResponseFlag) {
|
|
if (stopResponseFlag) {
|
|
controller.abort('User: Stop Response');
|
|
controller.abort('User: Stop Response');
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ if (editorEnabled) {
|
|
|
|
+ enhancing = false;
|
|
|
|
+ streaming = false;
|
|
|
|
+ }
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -143,17 +194,41 @@
|
|
if (line !== '') {
|
|
if (line !== '') {
|
|
console.log(line);
|
|
console.log(line);
|
|
if (line === 'data: [DONE]') {
|
|
if (line === 'data: [DONE]') {
|
|
- // responseMessage.done = true;
|
|
|
|
|
|
+ if (editorEnabled) {
|
|
|
|
+ responseMessage.content = '<status title="Edited" done="true" />';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ responseMessage.done = true;
|
|
messages = messages;
|
|
messages = messages;
|
|
} else {
|
|
} else {
|
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
|
console.log(data);
|
|
console.log(data);
|
|
|
|
|
|
- if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
|
|
|
|
|
+ let deltaContent = data.choices[0]?.delta?.content ?? '';
|
|
|
|
+ if (responseMessage.content == '' && deltaContent == '\n') {
|
|
continue;
|
|
continue;
|
|
} else {
|
|
} else {
|
|
- responseMessage.content += data.choices[0].delta.content ?? '';
|
|
|
|
- messages = messages;
|
|
|
|
|
|
+ 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();
|
|
await tick();
|
|
}
|
|
}
|
|
@@ -205,7 +280,10 @@
|
|
} else {
|
|
} else {
|
|
selectedModelId = '';
|
|
selectedModelId = '';
|
|
}
|
|
}
|
|
|
|
+
|
|
loaded = true;
|
|
loaded = true;
|
|
|
|
+
|
|
|
|
+ scrollToBottom();
|
|
});
|
|
});
|
|
</script>
|
|
</script>
|
|
|
|
|
|
@@ -240,7 +318,7 @@
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
-<div class="flex flex-col items-center mb-2 flex-1">
|
|
|
|
|
|
+<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=" 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="mx-auto w-full md:px-0 h-full relative">
|
|
<div class=" flex flex-col h-full">
|
|
<div class=" flex flex-col h-full">
|
|
@@ -248,6 +326,7 @@
|
|
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
|
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
|
id="messages-container"
|
|
id="messages-container"
|
|
bind:this={messagesContainerElement}
|
|
bind:this={messagesContainerElement}
|
|
|
|
+ on:scroll={onScroll}
|
|
>
|
|
>
|
|
<div class=" h-full w-full flex flex-col">
|
|
<div class=" h-full w-full flex flex-col">
|
|
<div class="flex-1 p-1">
|
|
<div class="flex-1 p-1">
|
|
@@ -264,15 +343,37 @@
|
|
onSubmit={submitHandler}
|
|
onSubmit={submitHandler}
|
|
onStop={stopHandler}
|
|
onStop={stopHandler}
|
|
>
|
|
>
|
|
- <div slot="menu">
|
|
|
|
- <select
|
|
|
|
- class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-50"
|
|
|
|
- bind:value={selectedModelId}
|
|
|
|
- >
|
|
|
|
- {#each $models as model}
|
|
|
|
- <option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
|
|
- {/each}
|
|
|
|
- </select>
|
|
|
|
|
|
+ <div slot="menu" class="flex items-center justify-between gap-2 w-full">
|
|
|
|
+ <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>
|
|
</div>
|
|
</MessageInput>
|
|
</MessageInput>
|
|
</div>
|
|
</div>
|