mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
enh: note versioning
This commit is contained in:
parent
e8b23ef0c1
commit
463ac99e39
@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
export let json = false;
|
export let json = false;
|
||||||
export let raw = false;
|
export let raw = false;
|
||||||
|
export let editable = true;
|
||||||
|
|
||||||
export let preserveBreaks = false;
|
export let preserveBreaks = false;
|
||||||
export let generateAutoCompletion: Function = async () => null;
|
export let generateAutoCompletion: Function = async () => null;
|
||||||
@ -58,6 +59,16 @@
|
|||||||
throwOnError: false
|
throwOnError: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$: if (editor) {
|
||||||
|
editor.setOptions({
|
||||||
|
editable: editable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (value === null && html !== null && editor) {
|
||||||
|
editor.commands.setContent(html);
|
||||||
|
}
|
||||||
|
|
||||||
// Function to find the next template in the document
|
// Function to find the next template in the document
|
||||||
function findNextTemplate(doc, from = 0) {
|
function findNextTemplate(doc, from = 0) {
|
||||||
const patterns = [{ start: '{{', end: '}}' }];
|
const patterns = [{ start: '{{', end: '}}' }];
|
||||||
|
19
src/lib/components/icons/ArrowUturnLeft.svelte
Normal file
19
src/lib/components/icons/ArrowUturnLeft.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3"
|
||||||
|
/>
|
||||||
|
</svg>
|
19
src/lib/components/icons/ArrowUturnRight.svelte
Normal file
19
src/lib/components/icons/ArrowUturnRight.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '1.5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { marked } from 'marked';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { config, settings, showSidebar } from '$lib/stores';
|
import { config, settings, showSidebar } from '$lib/stores';
|
||||||
@ -62,6 +63,8 @@
|
|||||||
import SparklesSolid from '../icons/SparklesSolid.svelte';
|
import SparklesSolid from '../icons/SparklesSolid.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Bars3BottomLeft from '../icons/Bars3BottomLeft.svelte';
|
import Bars3BottomLeft from '../icons/Bars3BottomLeft.svelte';
|
||||||
|
import ArrowUturnLeft from '../icons/ArrowUturnLeft.svelte';
|
||||||
|
import ArrowUturnRight from '../icons/ArrowUturnRight.svelte';
|
||||||
|
|
||||||
export let id: null | string = null;
|
export let id: null | string = null;
|
||||||
|
|
||||||
@ -75,6 +78,7 @@
|
|||||||
html: '',
|
html: '',
|
||||||
md: ''
|
md: ''
|
||||||
},
|
},
|
||||||
|
versions: [],
|
||||||
files: null
|
files: null
|
||||||
},
|
},
|
||||||
meta: null,
|
meta: null,
|
||||||
@ -83,14 +87,15 @@
|
|||||||
|
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
let selectedVersion = 'note';
|
let versionIdx = null;
|
||||||
|
|
||||||
let recording = false;
|
let recording = false;
|
||||||
let displayMediaRecord = false;
|
let displayMediaRecord = false;
|
||||||
|
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
let enhancing = false;
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
@ -118,7 +123,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
debounceTimeout = setTimeout(async () => {
|
debounceTimeout = setTimeout(async () => {
|
||||||
if (!note) {
|
if (!note || enhancing || versionIdx !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +146,104 @@
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionToggleHandler = () => {};
|
function areContentsEqual(a, b) {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertNoteVersion(note) {
|
||||||
|
const current = {
|
||||||
|
json: note.data.content.json,
|
||||||
|
html: note.data.content.html,
|
||||||
|
md: note.data.content.md
|
||||||
|
};
|
||||||
|
const lastVersion = note.data.versions?.at(-1);
|
||||||
|
|
||||||
|
if (!lastVersion || !areContentsEqual(lastVersion, current)) {
|
||||||
|
note.data.versions = (note.data.versions ?? []).concat(current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aiEnhanceContent(content) {
|
||||||
|
const md = content.md + '_ai';
|
||||||
|
const html = marked.parse(md);
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: null,
|
||||||
|
html: html,
|
||||||
|
md: md
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enhanceNoteHandler() {
|
||||||
|
insertNoteVersion(note);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
enhancing = false;
|
||||||
|
versionIdx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContentByVersion(versionIdx) {
|
||||||
|
if (!note.data.versions?.length) return;
|
||||||
|
let idx = versionIdx;
|
||||||
|
|
||||||
|
if (idx === null) idx = note.data.versions.length - 1; // latest
|
||||||
|
const v = note.data.versions[idx];
|
||||||
|
|
||||||
|
note.data.content.json = v.json;
|
||||||
|
note.data.content.html = v.html;
|
||||||
|
note.data.content.md = v.md;
|
||||||
|
|
||||||
|
if (versionIdx === null) {
|
||||||
|
const lastVersion = note.data.versions.at(-1);
|
||||||
|
const currentContent = note.data.content;
|
||||||
|
|
||||||
|
if (areContentsEqual(lastVersion, currentContent)) {
|
||||||
|
// remove the last version
|
||||||
|
note.data.versions = note.data.versions.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
function versionNavigateHandler(direction) {
|
||||||
|
if (!note.data.versions || note.data.versions.length === 0) return;
|
||||||
|
|
||||||
|
if (versionIdx === null) {
|
||||||
|
// Get latest snapshots
|
||||||
|
const lastVersion = note.data.versions.at(-1);
|
||||||
|
const currentContent = note.data.content;
|
||||||
|
|
||||||
|
if (!areContentsEqual(lastVersion, currentContent)) {
|
||||||
|
// If the current content is different from the last version, insert a new version
|
||||||
|
insertNoteVersion(note);
|
||||||
|
versionIdx = note.data.versions.length - 1;
|
||||||
|
} else {
|
||||||
|
versionIdx = note.data.versions.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 'prev') {
|
||||||
|
if (versionIdx > 0) versionIdx -= 1;
|
||||||
|
} else if (direction === 'next') {
|
||||||
|
if (versionIdx < note.data.versions.length - 1) versionIdx += 1;
|
||||||
|
else versionIdx = null; // Reset to latest
|
||||||
|
|
||||||
|
if (versionIdx === note.data.versions.length - 1) {
|
||||||
|
// If we reach the latest version, reset to null
|
||||||
|
versionIdx = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentByVersion(versionIdx);
|
||||||
|
}
|
||||||
|
|
||||||
const uploadFileHandler = async (file) => {
|
const uploadFileHandler = async (file) => {
|
||||||
const tempItemId = uuidv4();
|
const tempItemId = uuidv4();
|
||||||
@ -428,7 +530,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class=" w-full flex flex-col {loading ? 'opacity-20' : ''}">
|
<div class=" w-full flex flex-col {loading ? 'opacity-20' : ''}">
|
||||||
<div class="shrink-0 w-full flex justify-between items-center px-4.5 pt-1 mb-1.5">
|
<div class="shrink-0 w-full flex justify-between items-center px-4.5 pt-1 mb-1.5">
|
||||||
<div class="w-full flex">
|
<div class="w-full flex items-center">
|
||||||
<input
|
<input
|
||||||
class="w-full text-2xl font-medium bg-transparent outline-hidden"
|
class="w-full text-2xl font-medium bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
@ -437,7 +539,34 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center gap-2">
|
||||||
|
{#if note.data?.versions?.length > 0}
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
||||||
|
<button
|
||||||
|
class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500"
|
||||||
|
on:click={() => {
|
||||||
|
versionNavigateHandler('prev');
|
||||||
|
}}
|
||||||
|
disabled={(versionIdx === null && note.data.versions.length === 0) ||
|
||||||
|
versionIdx === 0}
|
||||||
|
>
|
||||||
|
<ArrowUturnLeft className="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500"
|
||||||
|
on:click={() => {
|
||||||
|
versionNavigateHandler('next');
|
||||||
|
}}
|
||||||
|
disabled={versionIdx >= note.data.versions.length || versionIdx === null}
|
||||||
|
>
|
||||||
|
<ArrowUturnRight className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<NoteMenu
|
<NoteMenu
|
||||||
onDownload={(type) => {
|
onDownload={(type) => {
|
||||||
downloadHandler(type);
|
downloadHandler(type);
|
||||||
@ -524,6 +653,7 @@
|
|||||||
placeholder={$i18n.t('Write something...')}
|
placeholder={$i18n.t('Write something...')}
|
||||||
html={note.data?.content?.html}
|
html={note.data?.content?.html}
|
||||||
json={true}
|
json={true}
|
||||||
|
editable={versionIdx === null}
|
||||||
onChange={(content) => {
|
onChange={(content) => {
|
||||||
note.data.content.html = content.html;
|
note.data.content.html = content.html;
|
||||||
note.data.content.md = content.md;
|
note.data.content.md = content.md;
|
||||||
@ -609,18 +739,20 @@
|
|||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<Tooltip content={$i18n.t('Record')} placement="top">
|
||||||
class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 bg-white dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl"
|
<button
|
||||||
type="button"
|
class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 bg-white dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl"
|
||||||
>
|
type="button"
|
||||||
<MicSolid className="size-4.5" />
|
>
|
||||||
</button>
|
<MicSolid className="size-4.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</RecordMenu>
|
</RecordMenu>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer p-0.5 flex gap-0.5 rounded-full border border-gray-50 dark:border-gray-850 dark:bg-gray-850 transition shadow-xl"
|
class="cursor-pointer flex gap-0.5 rounded-full border border-gray-50 dark:border-gray-850 dark:bg-gray-850 transition shadow-xl"
|
||||||
>
|
>
|
||||||
<Tooltip content={$i18n.t('My Notes')} placement="top">
|
<!-- <Tooltip content={$i18n.t('My Notes')} placement="top">
|
||||||
<button
|
<button
|
||||||
class="p-2 size-8.5 flex justify-center items-center {selectedVersion === 'note'
|
class="p-2 size-8.5 flex justify-center items-center {selectedVersion === 'note'
|
||||||
? 'bg-gray-100 dark:bg-gray-800 '
|
? 'bg-gray-100 dark:bg-gray-800 '
|
||||||
@ -633,16 +765,13 @@
|
|||||||
>
|
>
|
||||||
<Bars3BottomLeft />
|
<Bars3BottomLeft />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip> -->
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Enhance Notes')} placement="top">
|
<Tooltip content={$i18n.t('Enhance')} placement="top">
|
||||||
<button
|
<button
|
||||||
class="p-2 size-8.5 flex justify-center items-center {selectedVersion === 'ai'
|
class="p-2.5 flex justify-center items-center hover:bg-gray-50 dark:hover:bg-gray-800 rounded-full transition shrink-0"
|
||||||
? 'bg-gray-100 dark:bg-gray-800 '
|
|
||||||
: ' hover:bg-gray-50 dark:hover:bg-gray-800'} rounded-full transition shrink-0"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedVersion = 'ai';
|
enhanceNoteHandler();
|
||||||
versionToggleHandler();
|
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user