From deb1f6b6d709fd17836c67d99154a486edc27f0e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 5 May 2025 09:22:54 +0400 Subject: [PATCH] feat: ai enhanced notes --- src/lib/components/notes/NoteEditor.svelte | 206 +++++++++++++++++---- 1 file changed, 174 insertions(+), 32 deletions(-) diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index fa878b2d0..a14682d0a 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -12,11 +12,11 @@ import { marked } from 'marked'; import { toast } from 'svelte-sonner'; - import { config, settings, showSidebar } from '$lib/stores'; + import { config, models, settings, showSidebar } from '$lib/stores'; import { goto } from '$app/navigation'; - import { compressImage, copyToClipboard } from '$lib/utils'; - import { WEBUI_API_BASE_URL } from '$lib/constants'; + 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'; @@ -65,6 +65,10 @@ import Bars3BottomLeft from '../icons/Bars3BottomLeft.svelte'; import ArrowUturnLeft from '../icons/ArrowUturnLeft.svelte'; import ArrowUturnRight from '../icons/ArrowUturnRight.svelte'; + 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; @@ -88,14 +92,19 @@ let files = []; let versionIdx = null; + let selectedModelId = null; + let recording = false; let displayMediaRecord = false; + let showSettings = false; let showDeleteConfirm = false; - let dragged = false; + let dragged = false; let loading = false; + let enhancing = false; + let streaming = false; const init = async () => { loading = true; @@ -165,29 +174,22 @@ return false; } - async function aiEnhanceContent(content) { - // fake delay - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const md = content.md + '_ai'; - const html = marked.parse(md); - - return { - json: null, - html: html, - md: md - }; - } - async function enhanceNoteHandler() { - insertNoteVersion(note); + if (selectedModelId === '') { + toast.error($i18n.t('Please select a model.')); + return; + } + + const model = $models.find((model) => model.id === selectedModelId); + if (!model) { + selectedModelId = ''; + return; + } enhancing = true; - const aiResult = await aiEnhanceContent(note.data.content); - note.data.content.json = aiResult.json; - note.data.content.html = aiResult.html; - note.data.content.md = aiResult.md; + insertNoteVersion(note); + await enhanceCompletionHandler(model); enhancing = false; versionIdx = null; @@ -456,6 +458,96 @@ } }; + const enhanceCompletionHandler = async (model) => { + let enhancedContent = { + json: null, + html: '', + md: '' + }; + + const systemPrompt = `Enhance existing notes using additional context provided from audio transcription or uploaded file content. Your task is to make the notes more useful and comprehensive by incorporating relevant information from the provided context. + +Input will be provided within and XML tags, providing a structure for the existing notes and context respectively. + +# Output Format + +Provide the enhanced notes in markdown format. Use markdown syntax for headings, lists, and emphasis to improve clarity and presentation. Ensure that all integrated content from the context is accurately reflected. Return only the markdown formatted note. +`; + + const [res, controller] = await chatCompletion( + localStorage.token, + { + model: model.id, + stream: true, + messages: [ + { + role: 'system', + content: systemPrompt + }, + { + role: 'user', + content: + `${note.data.content.md}` + + (files && files.length > 0 + ? `\n${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}` + : '') + } + ] + }, + `${WEBUI_BASE_URL}/api` + ); + + await tick(); + + streaming = true; + + 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) { + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + if (line === 'data: [DONE]') { + console.log(line); + } else { + let data = JSON.parse(line.replace(/^data: /, '')); + console.log(data); + + if (data.choices && data.choices.length > 0) { + const choice = data.choices[0]; + if (choice.delta && choice.delta.content) { + enhancedContent.md += choice.delta.content; + enhancedContent.html = marked.parse(enhancedContent.md); + + note.data.content.md = enhancedContent.md; + note.data.content.html = enhancedContent.html; + note.data.content.json = null; + } + } + } + } + } + } catch (error) { + console.log(error); + } + } + } + + streaming = false; + }; + const onDragOver = (e) => { e.preventDefault(); @@ -489,6 +581,14 @@ onMount(async () => { await tick(); + if ($settings?.models) { + selectedModelId = $settings?.models[0]; + } else if ($config?.default_models) { + selectedModelId = $config?.default_models.split(',')[0]; + } else { + selectedModelId = ''; + } + const dropzoneElement = document.getElementById('note-editor'); dropzoneElement?.addEventListener('dragover', onDragOver); @@ -524,6 +624,42 @@
+ +
+
+
Settings
+ +
+ +
+
+ +
+
+
Model
+ +
+ +
+
+
+
+
+ {#if loading}
@@ -542,7 +678,7 @@ required /> -
+
{#if note.data?.versions?.length > 0}
@@ -588,13 +724,17 @@ showDeleteConfirm = true; }} > - + + +
@@ -618,7 +758,9 @@
{#if enhancing}
{/if} @@ -674,7 +816,7 @@