From 9a064d962315ebb1aaffade26019ad80a2e7c176 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 8 Jul 2025 12:31:31 +0400 Subject: [PATCH] feat: edit notes via chat --- .../chat/Messages/Markdown/HTMLToken.svelte | 17 ++ src/lib/components/notes/NoteEditor.svelte | 33 +-- .../components/notes/NoteEditor/Chat.svelte | 189 ++++++++++++++---- .../notes/NoteEditor/Chat/Message.svelte | 2 +- 4 files changed, 181 insertions(+), 60 deletions(-) diff --git a/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte index f917badac9..d246bb36d9 100644 --- a/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte +++ b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte @@ -84,6 +84,23 @@ {:else} {token.text} {/if} + {:else if token.text && token.text.includes('/)} + {@const statusTitle = match && match[1]} + {@const statusDone = match && match[2] === 'true'} + {#if statusTitle} +
+
+ {statusTitle} +
+
+ {:else} + {token.text} + {/if} {:else if token.text.includes(` @@ -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'} diff --git a/src/lib/components/notes/NoteEditor/Chat.svelte b/src/lib/components/notes/NoteEditor/Chat.svelte index a248ce2d9b..7e88f98b7b 100644 --- a/src/lib/components/notes/NoteEditor/Chat.svelte +++ b/src/lib/components/notes/NoteEditor/Chat.svelte @@ -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 XML tags. +- Additional context: Enclosed within 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 += + `${note?.data?.content?.md ?? ''}` + + (files && files.length > 0 + ? `\n${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}` + : ''); + + 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 = ''; + } + + 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 ?? ''; - 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 = ''; + + scrollToBottomHandler(); + messages = messages; + } else { + messageContent += deltaContent; + + responseMessage.content = messageContent; + messages = messages; + } await tick(); } @@ -205,7 +280,10 @@ } else { selectedModelId = ''; } + loaded = true; + + scrollToBottom(); }); @@ -240,7 +318,7 @@ -
+
@@ -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} >
@@ -264,15 +343,37 @@ onSubmit={submitHandler} onStop={stopHandler} > -
- +
+
+ + + +
+ + + +
diff --git a/src/lib/components/notes/NoteEditor/Chat/Message.svelte b/src/lib/components/notes/NoteEditor/Chat/Message.svelte index ffc88d0d2b..687ddaee77 100644 --- a/src/lib/components/notes/NoteEditor/Chat/Message.svelte +++ b/src/lib/components/notes/NoteEditor/Chat/Message.svelte @@ -95,7 +95,7 @@ }} /> {:else} -
+
{/if}