enh: note versioning

This commit is contained in:
Timothy Jaeryang Baek 2025-05-05 01:35:43 +04:00
parent e8b23ef0c1
commit 463ac99e39
4 changed files with 199 additions and 21 deletions

View File

@ -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: '}}' }];

View 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>

View 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>

View File

@ -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"
> >