浏览代码

enh: note versioning

Timothy Jaeryang Baek 5 月之前
父节点
当前提交
463ac99e39

+ 11 - 0
src/lib/components/common/RichTextInput.svelte

@@ -43,6 +43,7 @@
 
 	export let json = false;
 	export let raw = false;
+	export let editable = true;
 
 	export let preserveBreaks = false;
 	export let generateAutoCompletion: Function = async () => null;
@@ -58,6 +59,16 @@
 		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 findNextTemplate(doc, from = 0) {
 		const patterns = [{ start: '{{', end: '}}' }];

+ 19 - 0
src/lib/components/icons/ArrowUturnLeft.svelte

@@ -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 - 0
src/lib/components/icons/ArrowUturnRight.svelte

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

+ 150 - 21
src/lib/components/notes/NoteEditor.svelte

@@ -9,6 +9,7 @@
 
 	const i18n = getContext('i18n');
 
+	import { marked } from 'marked';
 	import { toast } from 'svelte-sonner';
 
 	import { config, settings, showSidebar } from '$lib/stores';
@@ -62,6 +63,8 @@
 	import SparklesSolid from '../icons/SparklesSolid.svelte';
 	import Tooltip from '../common/Tooltip.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;
 
@@ -75,6 +78,7 @@
 				html: '',
 				md: ''
 			},
+			versions: [],
 			files: null
 		},
 		meta: null,
@@ -83,14 +87,15 @@
 
 	let files = [];
 
-	let selectedVersion = 'note';
-
+	let versionIdx = null;
 	let recording = false;
 	let displayMediaRecord = false;
 
 	let showDeleteConfirm = false;
 	let dragged = false;
+
 	let loading = false;
+	let enhancing = false;
 
 	const init = async () => {
 		loading = true;
@@ -118,7 +123,7 @@
 		}
 
 		debounceTimeout = setTimeout(async () => {
-			if (!note) {
+			if (!note || enhancing || versionIdx !== null) {
 				return;
 			}
 
@@ -141,7 +146,104 @@
 		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 tempItemId = uuidv4();
@@ -428,7 +530,7 @@
 	{:else}
 		<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="w-full flex">
+				<div class="w-full flex items-center">
 					<input
 						class="w-full text-2xl font-medium bg-transparent outline-hidden"
 						type="text"
@@ -437,7 +539,34 @@
 						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
 							onDownload={(type) => {
 								downloadHandler(type);
@@ -524,6 +653,7 @@
 					placeholder={$i18n.t('Write something...')}
 					html={note.data?.content?.html}
 					json={true}
+					editable={versionIdx === null}
 					onChange={(content) => {
 						note.data.content.html = content.html;
 						note.data.content.md = content.md;
@@ -609,18 +739,20 @@
 					};
 				}}
 			>
-				<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>
+				<Tooltip content={$i18n.t('Record')} placement="top">
+					<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>
+				</Tooltip>
 			</RecordMenu>
 
 			<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
 						class="p-2 size-8.5 flex justify-center items-center {selectedVersion === 'note'
 							? 'bg-gray-100 dark:bg-gray-800 '
@@ -633,16 +765,13 @@
 					>
 						<Bars3BottomLeft />
 					</button>
-				</Tooltip>
+				</Tooltip> -->
 
-				<Tooltip content={$i18n.t('Enhance Notes')} placement="top">
+				<Tooltip content={$i18n.t('Enhance')} placement="top">
 					<button
-						class="p-2 size-8.5 flex justify-center items-center {selectedVersion === 'ai'
-							? 'bg-gray-100 dark:bg-gray-800 '
-							: ' hover:bg-gray-50 dark:hover:bg-gray-800'} rounded-full transition shrink-0"
+						class="p-2.5 flex justify-center items-center hover:bg-gray-50 dark:hover:bg-gray-800 rounded-full transition shrink-0"
 						on:click={() => {
-							selectedVersion = 'ai';
-							versionToggleHandler();
+							enhanceNoteHandler();
 						}}
 						type="button"
 					>