mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
refac: note editor
This commit is contained in:
parent
3392e2ef15
commit
a31a1f3c0d
4 changed files with 466 additions and 358 deletions
|
|
@ -98,7 +98,7 @@
|
||||||
let recording = false;
|
let recording = false;
|
||||||
let displayMediaRecord = false;
|
let displayMediaRecord = false;
|
||||||
|
|
||||||
let showSettings = false;
|
let showPanel = false;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
@ -672,6 +672,11 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import NotePanel from '$lib/components/notes/NotePanel.svelte';
|
||||||
|
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||||
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
import MenuLines from '../icons/MenuLines.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FilesOverlay show={dragged} />
|
<FilesOverlay show={dragged} />
|
||||||
|
|
@ -689,293 +694,272 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
||||||
<div class="relative flex-1 w-full h-full flex justify-center" id="note-editor">
|
<PaneGroup direction="horizontal" class="w-full h-full">
|
||||||
<Sidebar bind:show={showSettings} className=" bg-white dark:bg-gray-900" width="300px">
|
<Pane defaultSize={70} minSize={50} class="h-full flex flex-col w-full relative">
|
||||||
<div class="flex flex-col px-5 py-3 text-sm">
|
<div class="relative flex-1 w-full h-full flex justify-center pt-[11px]" id="note-editor">
|
||||||
<div class="flex justify-between items-center mb-2">
|
{#if loading}
|
||||||
<div class=" font-medium text-base">Settings</div>
|
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||||
|
<div class="m-auto">
|
||||||
<div class=" translate-x-1.5">
|
<Spinner className="size-5" />
|
||||||
<button
|
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
|
||||||
on:click={() => {
|
|
||||||
showSettings = !showSettings;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowRight className="size-3" strokeWidth="2.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1">
|
|
||||||
<div>
|
|
||||||
<div class=" text-xs font-medium mb-1">Model</div>
|
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<select
|
|
||||||
class="w-full bg-transparent text-sm outline-hidden"
|
|
||||||
bind:value={selectedModelId}
|
|
||||||
>
|
|
||||||
<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
|
|
||||||
{$i18n.t('Select a model')}
|
|
||||||
</option>
|
|
||||||
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
</div>
|
<div class=" w-full flex flex-col {loading ? 'opacity-20' : ''}">
|
||||||
</Sidebar>
|
<div class="shrink-0 w-full flex justify-between items-center px-3.5 mb-1.5">
|
||||||
|
<div class="w-full flex items-center">
|
||||||
|
<div
|
||||||
|
class="{$showSidebar
|
||||||
|
? 'md:hidden'
|
||||||
|
: ''} flex flex-none items-center pr-1 -translate-x-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="sidebar-toggle-button"
|
||||||
|
class="cursor-pointer p-1.5 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => {
|
||||||
|
showSidebar.set(!$showSidebar);
|
||||||
|
}}
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
>
|
||||||
|
<div class=" m-auto self-center">
|
||||||
|
<MenuLines />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
<input
|
||||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
class="w-full text-xl font-medium bg-transparent outline-hidden"
|
||||||
<div class="m-auto">
|
type="text"
|
||||||
<Spinner className="size-5" />
|
bind:value={note.title}
|
||||||
</div>
|
placeholder={$i18n.t('Title')}
|
||||||
</div>
|
required
|
||||||
{: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 mb-1.5">
|
|
||||||
<div class="w-full flex items-center">
|
|
||||||
<input
|
|
||||||
class="w-full text-2xl font-medium bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
bind:value={note.title}
|
|
||||||
placeholder={$i18n.t('Title')}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 translate-x-1">
|
<div class="flex items-center gap-2 translate-x-1">
|
||||||
{#if note.data?.versions?.length > 0}
|
{#if note.data?.versions?.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
||||||
<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"
|
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={() => {
|
on:click={() => {
|
||||||
versionNavigateHandler('prev');
|
versionNavigateHandler('prev');
|
||||||
}}
|
}}
|
||||||
disabled={(versionIdx === null && note.data.versions.length === 0) ||
|
disabled={(versionIdx === null && note.data.versions.length === 0) ||
|
||||||
versionIdx === 0}
|
versionIdx === 0}
|
||||||
>
|
>
|
||||||
<ArrowUturnLeft className="size-4" />
|
<ArrowUturnLeft className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
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={() => {
|
on:click={() => {
|
||||||
versionNavigateHandler('next');
|
versionNavigateHandler('next');
|
||||||
}}
|
}}
|
||||||
disabled={versionIdx >= note.data.versions.length || versionIdx === null}
|
disabled={versionIdx >= note.data.versions.length || versionIdx === null}
|
||||||
>
|
>
|
||||||
<ArrowUturnRight className="size-4" />
|
<ArrowUturnRight className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<NoteMenu
|
||||||
|
onDownload={(type) => {
|
||||||
|
downloadHandler(type);
|
||||||
|
}}
|
||||||
|
onCopyToClipboard={async () => {
|
||||||
|
const res = await copyToClipboard(note.data.content.md).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Copied to clipboard'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
showDeleteConfirm = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontal className="size-5" />
|
||||||
|
</NoteMenu>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
showPanel = !showPanel;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cog6 />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" mb-2.5 px-2.5">
|
||||||
|
<div
|
||||||
|
class="flex gap-1 items-center text-xs font-medium text-gray-500 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
<button class=" flex items-center gap-1 w-fit py-1 px-1.5 rounded-lg">
|
||||||
|
<Calendar className="size-3.5" strokeWidth="2" />
|
||||||
|
|
||||||
|
<span>{dayjs(note.created_at / 1000000).calendar()}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class=" flex items-center gap-1 w-fit py-1 px-1.5 rounded-lg">
|
||||||
|
<Users className="size-3.5" strokeWidth="2" />
|
||||||
|
|
||||||
|
<span> You </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" flex-1 w-full h-full overflow-auto px-3.5 pb-20 relative"
|
||||||
|
id="note-content-container"
|
||||||
|
>
|
||||||
|
{#if enhancing}
|
||||||
|
<div
|
||||||
|
class="w-full h-full fixed top-0 left-0 {streaming
|
||||||
|
? ''
|
||||||
|
: ' backdrop-blur-xs bg-white/10 dark:bg-gray-900/10'} flex items-center justify-center z-10 cursor-not-allowed"
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if files && files.length > 0}
|
||||||
|
<div class="mb-2.5 w-full flex gap-1 flex-wrap z-40">
|
||||||
|
{#each files as file, fileIdx}
|
||||||
|
<div class="w-fit">
|
||||||
|
{#if file.type === 'image'}
|
||||||
|
<Image
|
||||||
|
src={file.url}
|
||||||
|
imageClassName=" max-h-96 rounded-lg"
|
||||||
|
dismissible={true}
|
||||||
|
onDismiss={() => {
|
||||||
|
files = files.filter((item, idx) => idx !== fileIdx);
|
||||||
|
note.data.files = files.length > 0 ? files : null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<FileItem
|
||||||
|
item={file}
|
||||||
|
dismissible={true}
|
||||||
|
url={file.url}
|
||||||
|
name={file.name}
|
||||||
|
type={file.type}
|
||||||
|
size={file?.size}
|
||||||
|
loading={file.status === 'uploading'}
|
||||||
|
on:dismiss={() => {
|
||||||
|
files = files.filter((item) => item?.id !== file.id);
|
||||||
|
note.data.files = files.length > 0 ? files : null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<NoteMenu
|
<RichTextInput
|
||||||
onDownload={(type) => {
|
className="input-prose-sm px-0.5"
|
||||||
downloadHandler(type);
|
bind:value={note.data.content.json}
|
||||||
|
html={note.data?.content?.html}
|
||||||
|
json={true}
|
||||||
|
placeholder={$i18n.t('Write something...')}
|
||||||
|
editable={versionIdx === null && !enhancing}
|
||||||
|
onChange={(content) => {
|
||||||
|
note.data.content.html = content.html;
|
||||||
|
note.data.content.md = content.md;
|
||||||
}}
|
}}
|
||||||
onCopyToClipboard={async () => {
|
/>
|
||||||
const res = await copyToClipboard(note.data.content.md).catch((error) => {
|
|
||||||
toast.error(`${error}`);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
toast.success($i18n.t('Copied to clipboard'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDelete={() => {
|
|
||||||
showDeleteConfirm = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EllipsisHorizontal className="size-5" />
|
|
||||||
</NoteMenu>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
|
||||||
on:click={() => {
|
|
||||||
showSettings = !showSettings;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Cog6 />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<div class=" mb-2.5 px-3.5">
|
|
||||||
<div class="flex gap-1 items-center text-xs font-medium text-gray-500 dark:text-gray-500">
|
|
||||||
<button class=" flex items-center gap-1 w-fit py-1 px-1.5 rounded-lg">
|
|
||||||
<Calendar className="size-3.5" strokeWidth="2" />
|
|
||||||
|
|
||||||
<span>{dayjs(note.created_at / 1000000).calendar()}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class=" flex items-center gap-1 w-fit py-1 px-1.5 rounded-lg">
|
|
||||||
<Users className="size-3.5" strokeWidth="2" />
|
|
||||||
|
|
||||||
<span> You </span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class=" flex-1 w-full h-full overflow-auto px-4 pb-20 relative"
|
|
||||||
id="note-content-container"
|
|
||||||
>
|
|
||||||
{#if enhancing}
|
|
||||||
<div
|
|
||||||
class="w-full h-full fixed top-0 left-0 {streaming
|
|
||||||
? ''
|
|
||||||
: ' backdrop-blur-xs bg-white/10 dark:bg-gray-900/10'} flex items-center justify-center z-10 cursor-not-allowed"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if files && files.length > 0}
|
|
||||||
<div class="mb-3.5 mt-1.5 w-full flex gap-1 flex-wrap z-40">
|
|
||||||
{#each files as file, fileIdx}
|
|
||||||
<div class="w-fit">
|
|
||||||
{#if file.type === 'image'}
|
|
||||||
<Image
|
|
||||||
src={file.url}
|
|
||||||
imageClassName=" max-h-96 rounded-lg"
|
|
||||||
dismissible={true}
|
|
||||||
onDismiss={() => {
|
|
||||||
files = files.filter((item, idx) => idx !== fileIdx);
|
|
||||||
note.data.files = files.length > 0 ? files : null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<FileItem
|
|
||||||
item={file}
|
|
||||||
dismissible={true}
|
|
||||||
url={file.url}
|
|
||||||
name={file.name}
|
|
||||||
type={file.type}
|
|
||||||
size={file?.size}
|
|
||||||
loading={file.status === 'uploading'}
|
|
||||||
on:dismiss={() => {
|
|
||||||
files = files.filter((item) => item?.id !== file.id);
|
|
||||||
note.data.files = files.length > 0 ? files : null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<RichTextInput
|
|
||||||
className="input-prose-sm px-0.5"
|
|
||||||
bind:value={note.data.content.json}
|
|
||||||
html={note.data?.content?.html}
|
|
||||||
json={true}
|
|
||||||
placeholder={$i18n.t('Write something...')}
|
|
||||||
editable={versionIdx === null && !enhancing}
|
|
||||||
onChange={(content) => {
|
|
||||||
note.data.content.html = content.html;
|
|
||||||
note.data.content.md = content.md;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="absolute z-20 bottom-0 right-0 p-5 max-w-full w-full flex justify-end">
|
||||||
</div>
|
<div class="flex gap-1 justify-between w-full max-w-full">
|
||||||
|
{#if recording}
|
||||||
|
<div class="flex-1 w-full">
|
||||||
|
<VoiceRecording
|
||||||
|
bind:recording
|
||||||
|
className="p-1 w-full max-w-full"
|
||||||
|
transcribe={false}
|
||||||
|
displayMedia={displayMediaRecord}
|
||||||
|
onCancel={() => {
|
||||||
|
recording = false;
|
||||||
|
displayMediaRecord = false;
|
||||||
|
}}
|
||||||
|
onConfirm={(data) => {
|
||||||
|
if (data?.file) {
|
||||||
|
uploadFileHandler(data?.file);
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
recording = false;
|
||||||
class="absolute z-20 bottom-0 right-0 p-5 max-w-full {$showSidebar
|
displayMediaRecord = false;
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
}}
|
||||||
: ''} w-full flex justify-end"
|
/>
|
||||||
>
|
</div>
|
||||||
<div class="flex gap-1 justify-between w-full max-w-full">
|
{:else}
|
||||||
{#if recording}
|
<RecordMenu
|
||||||
<div class="flex-1 w-full">
|
onRecord={async () => {
|
||||||
<VoiceRecording
|
displayMediaRecord = false;
|
||||||
bind:recording
|
|
||||||
className="p-1 w-full max-w-full"
|
|
||||||
transcribe={false}
|
|
||||||
displayMedia={displayMediaRecord}
|
|
||||||
onCancel={() => {
|
|
||||||
recording = false;
|
|
||||||
displayMediaRecord = false;
|
|
||||||
}}
|
|
||||||
onConfirm={(data) => {
|
|
||||||
if (data?.file) {
|
|
||||||
uploadFileHandler(data?.file);
|
|
||||||
}
|
|
||||||
|
|
||||||
recording = false;
|
try {
|
||||||
displayMediaRecord = false;
|
let stream = await navigator.mediaDevices
|
||||||
}}
|
.getUserMedia({ audio: true })
|
||||||
/>
|
.catch(function (err) {
|
||||||
</div>
|
toast.error(
|
||||||
{:else}
|
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
||||||
<RecordMenu
|
error: err
|
||||||
onRecord={async () => {
|
})
|
||||||
displayMediaRecord = false;
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
if (stream) {
|
||||||
let stream = await navigator.mediaDevices
|
recording = true;
|
||||||
.getUserMedia({ audio: true })
|
const tracks = stream.getTracks();
|
||||||
.catch(function (err) {
|
tracks.forEach((track) => track.stop());
|
||||||
toast.error(
|
}
|
||||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
stream = null;
|
||||||
error: err
|
} catch {
|
||||||
})
|
toast.error($i18n.t('Permission denied when accessing microphone'));
|
||||||
);
|
}
|
||||||
return null;
|
}}
|
||||||
});
|
onCaptureAudio={async () => {
|
||||||
|
displayMediaRecord = true;
|
||||||
|
|
||||||
if (stream) {
|
|
||||||
recording = true;
|
recording = true;
|
||||||
const tracks = stream.getTracks();
|
}}
|
||||||
tracks.forEach((track) => track.stop());
|
onUpload={async () => {
|
||||||
}
|
const input = document.createElement('input');
|
||||||
stream = null;
|
input.type = 'file';
|
||||||
} catch {
|
input.accept = 'audio/*';
|
||||||
toast.error($i18n.t('Permission denied when accessing microphone'));
|
input.multiple = false;
|
||||||
}
|
input.click();
|
||||||
}}
|
|
||||||
onCaptureAudio={async () => {
|
|
||||||
displayMediaRecord = true;
|
|
||||||
|
|
||||||
recording = true;
|
input.onchange = async (e) => {
|
||||||
}}
|
const files = e.target.files;
|
||||||
onUpload={async () => {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept = 'audio/*';
|
|
||||||
input.multiple = false;
|
|
||||||
input.click();
|
|
||||||
|
|
||||||
input.onchange = async (e) => {
|
if (files && files.length > 0) {
|
||||||
const files = e.target.files;
|
await uploadFileHandler(files[0]);
|
||||||
|
}
|
||||||
if (files && files.length > 0) {
|
};
|
||||||
await uploadFileHandler(files[0]);
|
}}
|
||||||
}
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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" />
|
<Tooltip content={$i18n.t('Record')} placement="top">
|
||||||
</button>
|
<button
|
||||||
</Tooltip>
|
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"
|
||||||
</RecordMenu>
|
type="button"
|
||||||
|
>
|
||||||
|
<MicSolid className="size-4.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</RecordMenu>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer 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 '
|
||||||
|
|
@ -990,25 +974,60 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
</button>
|
</button>
|
||||||
</Tooltip> -->
|
</Tooltip> -->
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Enhance')} placement="top">
|
<Tooltip content={$i18n.t('Enhance')} placement="top">
|
||||||
<button
|
<button
|
||||||
class="{enhancing
|
class="{enhancing
|
||||||
? 'p-2'
|
? 'p-2'
|
||||||
: 'p-2.5'} flex justify-center items-center hover:bg-gray-50 dark:hover:bg-gray-800 rounded-full transition shrink-0"
|
: 'p-2.5'} flex justify-center items-center hover:bg-gray-50 dark:hover:bg-gray-800 rounded-full transition shrink-0"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
enhanceNoteHandler();
|
enhanceNoteHandler();
|
||||||
}}
|
}}
|
||||||
disabled={enhancing}
|
disabled={enhancing}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if enhancing}
|
{#if enhancing}
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
{:else}
|
{:else}
|
||||||
<SparklesSolid />
|
<SparklesSolid />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</Pane>
|
||||||
</div>
|
<NotePanel bind:show={showPanel}>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class=" -translate-x-1.5">
|
||||||
|
<button
|
||||||
|
class="p-1.5 bg-transparent transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
showPanel = !showPanel;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-5" strokeWidth="2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" font-medium text-base">Settings</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div>
|
||||||
|
<div class=" text-xs font-medium mb-1">Model</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
|
||||||
|
<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
|
||||||
|
{$i18n.t('Select a model')}
|
||||||
|
</option>
|
||||||
|
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
|
||||||
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NotePanel>
|
||||||
|
</PaneGroup>
|
||||||
|
|
|
||||||
82
src/lib/components/notes/NotePanel.svelte
Normal file
82
src/lib/components/notes/NotePanel.svelte
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { Pane, PaneResizer } from 'paneforge';
|
||||||
|
|
||||||
|
import Drawer from '../common/Drawer.svelte';
|
||||||
|
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let pane = null;
|
||||||
|
|
||||||
|
export let containerId = 'note-editor';
|
||||||
|
|
||||||
|
let mediaQuery;
|
||||||
|
let largeScreen = false;
|
||||||
|
|
||||||
|
let minSize = 0;
|
||||||
|
|
||||||
|
const handleMediaQuery = async (e) => {
|
||||||
|
if (e.matches) {
|
||||||
|
largeScreen = true;
|
||||||
|
} else {
|
||||||
|
largeScreen = false;
|
||||||
|
pane = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// listen to resize 1024px
|
||||||
|
mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleMediaQuery);
|
||||||
|
handleMediaQuery(mediaQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
mediaQuery.removeEventListener('change', handleMediaQuery);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !largeScreen}
|
||||||
|
{#if show}
|
||||||
|
<Drawer
|
||||||
|
{show}
|
||||||
|
onClose={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" px-3.5 py-2.5 h-screen">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
{/if}
|
||||||
|
{:else if show}
|
||||||
|
<PaneResizer
|
||||||
|
class="relative flex w-2 items-center justify-center bg-background group bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850"
|
||||||
|
>
|
||||||
|
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
||||||
|
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||||
|
</div>
|
||||||
|
</PaneResizer>
|
||||||
|
|
||||||
|
<Pane
|
||||||
|
bind:pane
|
||||||
|
defaultSize={30}
|
||||||
|
minSize={30}
|
||||||
|
onCollapse={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
collapsible={true}
|
||||||
|
class=" z-10 "
|
||||||
|
>
|
||||||
|
{#if show}
|
||||||
|
<div class="flex max-h-full min-h-full">
|
||||||
|
<div
|
||||||
|
class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Pane>
|
||||||
|
{/if}
|
||||||
|
|
@ -32,73 +32,5 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div
|
<slot />
|
||||||
class=" flex flex-col w-full h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
|
||||||
: ''} max-w-full"
|
|
||||||
>
|
|
||||||
<nav class=" px-2 pt-1 backdrop-blur-xl w-full drag-region">
|
|
||||||
<div class=" flex items-center">
|
|
||||||
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
|
||||||
<button
|
|
||||||
id="sidebar-toggle-button"
|
|
||||||
class="cursor-pointer p-1.5 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
|
||||||
on:click={() => {
|
|
||||||
showSidebar.set(!$showSidebar);
|
|
||||||
}}
|
|
||||||
aria-label="Toggle Sidebar"
|
|
||||||
>
|
|
||||||
<div class=" m-auto self-center">
|
|
||||||
<MenuLines />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-2 py-0.5 self-center flex items-center justify-between w-full">
|
|
||||||
<div class="">
|
|
||||||
<div
|
|
||||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium bg-transparent py-1 touch-auto pointer-events-auto"
|
|
||||||
>
|
|
||||||
<a class="min-w-fit transition" href="/notes">
|
|
||||||
{$i18n.t('Notes')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" self-center flex items-center gap-1">
|
|
||||||
{#if $user !== undefined && $user !== null}
|
|
||||||
<UserMenu
|
|
||||||
className="max-w-[240px]"
|
|
||||||
role={$user?.role}
|
|
||||||
help={true}
|
|
||||||
on:show={(e) => {
|
|
||||||
if (e.detail === 'archived-chat') {
|
|
||||||
showArchivedChats.set(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
|
||||||
aria-label="User Menu"
|
|
||||||
>
|
|
||||||
<div class=" self-center">
|
|
||||||
<img
|
|
||||||
src={$user?.profile_image_url}
|
|
||||||
class="size-6 object-cover rounded-full"
|
|
||||||
alt="User profile"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</UserMenu>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class=" pb-1 flex-1 max-h-full overflow-y-auto @container">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,80 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { showSidebar, user } from '$lib/stores';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
||||||
|
import UserMenu from '$lib/components/layout/Sidebar/UserMenu.svelte';
|
||||||
import Notes from '$lib/components/notes/Notes.svelte';
|
import Notes from '$lib/components/notes/Notes.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Notes />
|
<div
|
||||||
|
class=" flex flex-col w-full h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
||||||
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
|
: ''} max-w-full"
|
||||||
|
>
|
||||||
|
<nav class=" px-2 pt-1 backdrop-blur-xl w-full drag-region">
|
||||||
|
<div class=" flex items-center">
|
||||||
|
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
||||||
|
<button
|
||||||
|
id="sidebar-toggle-button"
|
||||||
|
class="cursor-pointer p-1.5 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => {
|
||||||
|
showSidebar.set(!$showSidebar);
|
||||||
|
}}
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
>
|
||||||
|
<div class=" m-auto self-center">
|
||||||
|
<MenuLines />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-2 py-0.5 self-center flex items-center justify-between w-full">
|
||||||
|
<div class="">
|
||||||
|
<div
|
||||||
|
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium bg-transparent py-1 touch-auto pointer-events-auto"
|
||||||
|
>
|
||||||
|
<a class="min-w-fit transition" href="/notes">
|
||||||
|
{$i18n.t('Notes')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" self-center flex items-center gap-1">
|
||||||
|
{#if $user !== undefined && $user !== null}
|
||||||
|
<UserMenu
|
||||||
|
className="max-w-[240px]"
|
||||||
|
role={$user?.role}
|
||||||
|
help={true}
|
||||||
|
on:show={(e) => {
|
||||||
|
if (e.detail === 'archived-chat') {
|
||||||
|
showArchivedChats.set(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
|
aria-label="User Menu"
|
||||||
|
>
|
||||||
|
<div class=" self-center">
|
||||||
|
<img
|
||||||
|
src={$user?.profile_image_url}
|
||||||
|
class="size-6 object-cover rounded-full"
|
||||||
|
alt="User profile"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</UserMenu>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class=" pb-1 flex-1 max-h-full overflow-y-auto @container">
|
||||||
|
<Notes />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue