enh: import md notes

This commit is contained in:
Timothy Jaeryang Baek 2025-05-04 23:04:15 +04:00
parent 8514669053
commit a6a7ce6b68
3 changed files with 231 additions and 126 deletions

View File

@ -165,6 +165,10 @@
// Usage example // Usage example
content = await tryParse(value); content = await tryParse(value);
} }
} else {
if (html && !content) {
content = html;
}
} }
console.log('content', content); console.log('content', content);

View File

@ -504,6 +504,7 @@
className="input-prose-sm px-0.5" className="input-prose-sm px-0.5"
bind:value={note.data.content.json} bind:value={note.data.content.json}
placeholder={$i18n.t('Write something...')} placeholder={$i18n.t('Write something...')}
html={note.data?.content?.html}
json={true} json={true}
onChange={(content) => { onChange={(content) => {
note.data.content.html = content.html; note.data.content.html = content.html;

View File

@ -28,7 +28,7 @@
$: loadLocale($i18n.languages); $: loadLocale($i18n.languages);
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte'; import { onMount, getContext, onDestroy } from 'svelte';
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores'; import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
import { createNewNote, deleteNoteById, getNotes } from '$lib/apis/notes'; import { createNewNote, deleteNoteById, getNotes } from '$lib/apis/notes';
@ -42,6 +42,8 @@
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import NoteMenu from './Notes/NoteMenu.svelte'; import NoteMenu from './Notes/NoteMenu.svelte';
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
import { marked } from 'marked';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let loaded = false; let loaded = false;
@ -166,9 +168,103 @@
} }
}; };
const inputFilesHandler = async (inputFiles) => {
// Check if all the file is a markdown file and extract name and content
for (const file of inputFiles) {
if (file.type !== 'text/markdown') {
toast.error($i18n.t('Only markdown files are allowed'));
return;
}
const reader = new FileReader();
reader.onload = async (event) => {
const content = event.target.result;
let name = file.name.replace(/\.md$/, '');
if (typeof content !== 'string') {
toast.error($i18n.t('Invalid file content'));
return;
}
// Create a new note with the content
const res = await createNewNote(localStorage.token, {
title: name,
data: {
content: {
json: null,
html: marked.parse(content ?? ''),
md: content
}
},
meta: null,
access_control: null
}).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
init();
}
};
reader.readAsText(file);
}
};
let dragged = false;
const onDragOver = (e) => {
e.preventDefault();
// Check if a file is being dragged.
if (e.dataTransfer?.types?.includes('Files')) {
dragged = true;
} else {
dragged = false;
}
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
}
}
dragged = false;
};
onDestroy(() => {
console.log('destroy');
const dropzoneElement = document.getElementById('notes-container');
if (dropzoneElement) {
dropzoneElement?.removeEventListener('dragover', onDragOver);
dropzoneElement?.removeEventListener('drop', onDrop);
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
}
});
onMount(async () => { onMount(async () => {
await init(); await init();
loaded = true; loaded = true;
const dropzoneElement = document.getElementById('notes-container');
dropzoneElement?.addEventListener('dragover', onDragOver);
dropzoneElement?.addEventListener('drop', onDrop);
dropzoneElement?.addEventListener('dragleave', onDragLeave);
}); });
</script> </script>
@ -178,136 +274,139 @@
</title> </title>
</svelte:head> </svelte:head>
{#if loaded} <FilesOverlay show={dragged} />
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
title={$i18n.t('Delete note?')}
on:confirm={() => {
deleteNoteHandler(selectedNote.id);
showDeleteConfirm = false;
}}
>
<div class=" text-sm text-gray-500">
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedNote.title}</span>.
</div>
</DeleteConfirmDialog>
<div class="px-4.5 @container h-full pt-2"> <div id="notes-container" class="w-full h-full">
{#if Object.keys(notes).length > 0} {#if loaded}
{#each Object.keys(notes) as timeRange} <DeleteConfirmDialog
<div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2.5"> bind:show={showDeleteConfirm}
{$i18n.t(timeRange)} title={$i18n.t('Delete note?')}
</div> on:confirm={() => {
deleteNoteHandler(selectedNote.id);
<div class="mb-5 gap-2.5 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> showDeleteConfirm = false;
{#each notes[timeRange] as note, idx (note.id)} }}
<div >
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 bg-gray-50 dark:bg-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition" <div class=" text-sm text-gray-500">
> {$i18n.t('This will delete')} <span class=" font-semibold">{selectedNote.title}</span>.
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a
href={`/notes/${note.id}`}
class="w-full -translate-y-0.5 flex flex-col justify-between"
>
<div class="flex-1">
<div class=" flex items-center gap-2 self-center mb-1 justify-between">
<div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
<div>
<NoteMenu
onDownload={(type) => {
selectedNote = note;
downloadHandler(type);
}}
onDelete={() => {
selectedNote = note;
showDeleteConfirm = true;
}}
>
<button
class="self-center w-fit text-sm p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
>
<EllipsisHorizontal className="size-5" />
</button>
</NoteMenu>
</div>
</div>
<div
class=" text-xs text-gray-500 dark:text-gray-500 mb-3 line-clamp-5 min-h-18"
>
{#if note.data?.content?.md}
{note.data?.content?.md}
{:else}
{$i18n.t('No content')}
{/if}
</div>
</div>
<div class=" text-xs px-0.5 w-full flex justify-between items-center">
<div>
{dayjs(note.updated_at / 1000000).fromNow()}
</div>
<Tooltip
content={note?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</Tooltip>
</div>
</a>
</div>
</div>
{/each}
</div>
{/each}
{:else}
<div class="w-full h-full flex flex-col items-center justify-center">
<div class="pb-20 text-center">
<div class=" text-xl font-medium text-gray-400 dark:text-gray-600">
{$i18n.t('No Notes')}
</div>
<div class="mt-1 text-sm text-gray-300 dark:text-gray-700">
{$i18n.t('Create your first note by clicking on the plus button below.')}
</div>
</div>
</div> </div>
{/if} </DeleteConfirmDialog>
</div>
<div class="absolute bottom-0 left-0 right-0 p-5 max-w-full flex justify-end"> <div class="px-4.5 @container h-full pt-2">
<div class="flex gap-0.5 justify-end w-full"> {#if Object.keys(notes).length > 0}
<Tooltip content={$i18n.t('Create Note')}> {#each Object.keys(notes) as timeRange}
<button <div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2.5">
class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl" {$i18n.t(timeRange)}
type="button" </div>
on:click={async () => {
createNoteHandler();
}}
>
<Plus className="size-4.5" strokeWidth="2.5" />
</button>
</Tooltip>
<!-- <button <div class="mb-5 gap-2.5 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{#each notes[timeRange] as note, idx (note.id)}
<div
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 bg-gray-50 dark:bg-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a
href={`/notes/${note.id}`}
class="w-full -translate-y-0.5 flex flex-col justify-between"
>
<div class="flex-1">
<div class=" flex items-center gap-2 self-center mb-1 justify-between">
<div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
<div>
<NoteMenu
onDownload={(type) => {
selectedNote = note;
downloadHandler(type);
}}
onDelete={() => {
selectedNote = note;
showDeleteConfirm = true;
}}
>
<button
class="self-center w-fit text-sm p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
>
<EllipsisHorizontal className="size-5" />
</button>
</NoteMenu>
</div>
</div>
<div
class=" text-xs text-gray-500 dark:text-gray-500 mb-3 line-clamp-5 min-h-18"
>
{#if note.data?.content?.md}
{note.data?.content?.md}
{:else}
{$i18n.t('No content')}
{/if}
</div>
</div>
<div class=" text-xs px-0.5 w-full flex justify-between items-center">
<div>
{dayjs(note.updated_at / 1000000).fromNow()}
</div>
<Tooltip
content={note?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</Tooltip>
</div>
</a>
</div>
</div>
{/each}
</div>
{/each}
{:else}
<div class="w-full h-full flex flex-col items-center justify-center">
<div class="pb-20 text-center">
<div class=" text-xl font-medium text-gray-400 dark:text-gray-600">
{$i18n.t('No Notes')}
</div>
<div class="mt-1 text-sm text-gray-300 dark:text-gray-700">
{$i18n.t('Create your first note by clicking on the plus button below.')}
</div>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 left-0 right-0 p-5 max-w-full flex justify-end">
<div class="flex gap-0.5 justify-end w-full">
<Tooltip content={$i18n.t('Create Note')}>
<button
class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl"
type="button"
on:click={async () => {
createNoteHandler();
}}
>
<Plus className="size-4.5" strokeWidth="2.5" />
</button>
</Tooltip>
<!-- <button
class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl" class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl"
> >
<SparklesSolid className="size-4" /> <SparklesSolid className="size-4" />
</button> --> </button> -->
</div>
</div> </div>
</div>
<!-- {#if $user?.role === 'admin'} <!-- {#if $user?.role === 'admin'}
<div class=" flex justify-end w-full mb-3"> <div class=" flex justify-end w-full mb-3">
<div class="flex space-x-2"> <div class="flex space-x-2">
<input <input
@ -357,8 +456,9 @@
</div> </div>
</div> </div>
{/if} --> {/if} -->
{:else} {:else}
<div class="w-full h-full flex justify-center items-center"> <div class="w-full h-full flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
{/if} {/if}
</div>