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}
|
||||
{token.text}
|
||||
{/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"`)}
|
||||
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
|
||||
{@const fileId = match && match[1]}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,8 @@
|
|||
import { marked } from 'marked';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { config, models, settings, showSidebar } from '$lib/stores';
|
||||
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 calendar from 'dayjs/plugin/calendar';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
|
|
@ -29,6 +24,21 @@
|
|||
dayjs.extend(duration);
|
||||
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) {
|
||||
for (const locale of locales) {
|
||||
try {
|
||||
|
|
@ -69,7 +79,6 @@
|
|||
import Sidebar from '../common/Sidebar.svelte';
|
||||
import ArrowRight from '../icons/ArrowRight.svelte';
|
||||
import Cog6 from '../icons/Cog6.svelte';
|
||||
import { chatCompletion } from '$lib/apis/openai';
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<FilesOverlay show={dragged} />
|
||||
|
|
@ -1046,8 +1047,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
|||
bind:show={showPanel}
|
||||
bind:selectedModelId
|
||||
bind:messages
|
||||
bind:note
|
||||
bind:enhancing
|
||||
bind:streaming
|
||||
{files}
|
||||
{note}
|
||||
onInsert={insertHandler}
|
||||
/>
|
||||
{:else if selectedPanel === 'settings'}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
export let show = false;
|
||||
export let selectedModelId = '';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -23,14 +24,21 @@
|
|||
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 note = null;
|
||||
|
||||
export let files = [];
|
||||
export let messages = [];
|
||||
|
||||
export let onInsert = (content) => {};
|
||||
export let scrollToBottomHandler = () => {};
|
||||
|
||||
let loaded = false;
|
||||
|
||||
|
|
@ -40,13 +48,43 @@
|
|||
let messagesContainerElement: HTMLDivElement;
|
||||
|
||||
let system = '';
|
||||
let editorEnabled = false;
|
||||
|
||||
let chatInputElement = null;
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = messagesContainerElement;
|
||||
const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
|
||||
|
||||
if (element) {
|
||||
element.scrollTop = element?.scrollHeight;
|
||||
## 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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -83,37 +121,43 @@
|
|||
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: [
|
||||
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`
|
||||
);
|
||||
|
|
@ -121,6 +165,8 @@
|
|||
await tick();
|
||||
scrollToBottom();
|
||||
|
||||
let messageContent = '';
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
|
|
@ -133,6 +179,11 @@
|
|||
if (stopResponseFlag) {
|
||||
controller.abort('User: Stop Response');
|
||||
}
|
||||
|
||||
if (editorEnabled) {
|
||||
enhancing = false;
|
||||
streaming = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -143,17 +194,41 @@
|
|||
if (line !== '') {
|
||||
console.log(line);
|
||||
if (line === 'data: [DONE]') {
|
||||
// responseMessage.done = true;
|
||||
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);
|
||||
|
||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||
let deltaContent = data.choices[0]?.delta?.content ?? '';
|
||||
if (responseMessage.content == '' && deltaContent == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||
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();
|
||||
}
|
||||
|
|
@ -205,7 +280,10 @@
|
|||
} else {
|
||||
selectedModelId = '';
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
|
||||
scrollToBottom();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -240,7 +318,7 @@
|
|||
</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="mx-auto w-full md:px-0 h-full relative">
|
||||
<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"
|
||||
id="messages-container"
|
||||
bind:this={messagesContainerElement}
|
||||
on:scroll={onScroll}
|
||||
>
|
||||
<div class=" h-full w-full flex flex-col">
|
||||
<div class="flex-1 p-1">
|
||||
|
|
@ -264,15 +343,37 @@
|
|||
onSubmit={submitHandler}
|
||||
onStop={stopHandler}
|
||||
>
|
||||
<div slot="menu">
|
||||
<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-50"
|
||||
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>
|
||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
||||
>{model.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</MessageInput>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class=" markdown-prose-sm">
|
||||
<div class=" markdown-prose-sm text-sm">
|
||||
<Markdown id={`note-message-${idx}`} content={message.content} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in a new issue