mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
feat: edit notes via chat
This commit is contained in:
parent
45085d04eb
commit
9a064d9623
4 changed files with 181 additions and 60 deletions
|
|
@ -84,6 +84,23 @@
|
||||||
{:else}
|
{:else}
|
||||||
{token.text}
|
{token.text}
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if token.text && token.text.includes('<status')}
|
||||||
|
{@const match = token.text.match(/<status title="([^"]+)" done="(true|false)" ?\/?>/)}
|
||||||
|
{@const statusTitle = match && match[1]}
|
||||||
|
{@const statusDone = match && match[2] === 'true'}
|
||||||
|
{#if statusTitle}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{statusDone === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
{statusTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{token.text}
|
||||||
|
{/if}
|
||||||
{:else if token.text.includes(`<file type="html"`)}
|
{:else if token.text.includes(`<file type="html"`)}
|
||||||
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
|
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
|
||||||
{@const fileId = match && match[1]}
|
{@const fileId = match && match[1]}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,8 @@
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { config, models, settings, showSidebar } from '$lib/stores';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { compressImage, copyToClipboard, splitStream } from '$lib/utils';
|
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
|
||||||
import { uploadFile } from '$lib/apis/files';
|
|
||||||
|
|
||||||
import dayjs from '$lib/dayjs';
|
import dayjs from '$lib/dayjs';
|
||||||
import calendar from 'dayjs/plugin/calendar';
|
import calendar from 'dayjs/plugin/calendar';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
|
@ -29,6 +24,21 @@
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||||
|
|
||||||
|
import { compressImage, copyToClipboard, splitStream } from '$lib/utils';
|
||||||
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { uploadFile } from '$lib/apis/files';
|
||||||
|
import { chatCompletion } from '$lib/apis/openai';
|
||||||
|
|
||||||
|
import { config, models, settings, showSidebar } from '$lib/stores';
|
||||||
|
|
||||||
|
import NotePanel from '$lib/components/notes/NotePanel.svelte';
|
||||||
|
import MenuLines from '../icons/MenuLines.svelte';
|
||||||
|
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
|
||||||
|
import Settings from './NoteEditor/Settings.svelte';
|
||||||
|
import Chat from './NoteEditor/Chat.svelte';
|
||||||
|
|
||||||
async function loadLocale(locales) {
|
async function loadLocale(locales) {
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -69,7 +79,6 @@
|
||||||
import Sidebar from '../common/Sidebar.svelte';
|
import Sidebar from '../common/Sidebar.svelte';
|
||||||
import ArrowRight from '../icons/ArrowRight.svelte';
|
import ArrowRight from '../icons/ArrowRight.svelte';
|
||||||
import Cog6 from '../icons/Cog6.svelte';
|
import Cog6 from '../icons/Cog6.svelte';
|
||||||
import { chatCompletion } from '$lib/apis/openai';
|
|
||||||
|
|
||||||
export let id: null | string = null;
|
export let id: null | string = null;
|
||||||
|
|
||||||
|
|
@ -682,14 +691,6 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
import NotePanel from '$lib/components/notes/NotePanel.svelte';
|
|
||||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
|
||||||
import XMark from '../icons/XMark.svelte';
|
|
||||||
import MenuLines from '../icons/MenuLines.svelte';
|
|
||||||
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
|
|
||||||
import Settings from './NoteEditor/Settings.svelte';
|
|
||||||
import Chat from './NoteEditor/Chat.svelte';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FilesOverlay show={dragged} />
|
<FilesOverlay show={dragged} />
|
||||||
|
|
@ -1046,8 +1047,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
bind:show={showPanel}
|
bind:show={showPanel}
|
||||||
bind:selectedModelId
|
bind:selectedModelId
|
||||||
bind:messages
|
bind:messages
|
||||||
|
bind:note
|
||||||
|
bind:enhancing
|
||||||
|
bind:streaming
|
||||||
{files}
|
{files}
|
||||||
{note}
|
|
||||||
onInsert={insertHandler}
|
onInsert={insertHandler}
|
||||||
/>
|
/>
|
||||||
{:else if selectedPanel === 'settings'}
|
{:else if selectedPanel === 'settings'}
|
||||||
|
|
|
||||||
|
|
@ -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 scrollToBottom = () => {
|
const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
|
||||||
const element = messagesContainerElement;
|
|
||||||
|
|
||||||
if (element) {
|
## Task
|
||||||
element.scrollTop = element?.scrollHeight;
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -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: [
|
messages: chatMessages
|
||||||
system
|
// ...(files && files.length > 0 ? { files } : {}) // TODO: Decide whether to use native file handling or not
|
||||||
? {
|
|
||||||
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
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
`${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 ?? '';
|
if (editorEnabled) {
|
||||||
messages = messages;
|
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">
|
<div slot="menu" class="flex items-center justify-between gap-2 w-full">
|
||||||
<select
|
<div>
|
||||||
class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-50"
|
<Tooltip content={$i18n.t('Edit')} placement="top">
|
||||||
bind:value={selectedModelId}
|
<button
|
||||||
>
|
on:click|preventDefault={() => (editorEnabled = !editorEnabled)}
|
||||||
{#each $models as model}
|
type="button"
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
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
|
||||||
{/each}
|
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
|
||||||
</select>
|
: '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>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" markdown-prose-sm">
|
<div class=" markdown-prose-sm text-sm">
|
||||||
<Markdown id={`note-message-${idx}`} content={message.content} />
|
<Markdown id={`note-message-${idx}`} content={message.content} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue