feat/enh: create note from input

This commit is contained in:
Timothy Jaeryang Baek 2025-12-09 20:49:46 -05:00
parent 65d4b22c7c
commit 00c2b6ca40
5 changed files with 93 additions and 20 deletions

View file

@ -135,7 +135,6 @@ async def search_notes(
async def create_new_note( async def create_new_note(
request: Request, form_data: NoteForm, user=Depends(get_verified_user) request: Request, form_data: NoteForm, user=Depends(get_verified_user)
): ):
if user.role != "admin" and not has_permission( if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
): ):
@ -187,9 +186,13 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
) )
write_access = has_access( write_access = (
user.role == "admin"
or (user.id == note.user_id)
or has_access(
user.id, type="write", access_control=note.access_control, strict=False user.id, type="write", access_control=note.access_control, strict=False
) )
)
return NoteResponse(**note.model_dump(), write_access=write_access) return NoteResponse(**note.model_dump(), write_access=write_access)

View file

@ -1,14 +1,22 @@
<script lang="ts"> <script lang="ts">
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { marked } from 'marked';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker'; import dayjs from '$lib/dayjs';
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker'; import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(duration);
dayjs.extend(relativeTime);
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte'; import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { import {
@ -49,6 +57,9 @@
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
import { createNoteHandler } from '../notes/utils';
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
import InputMenu from './MessageInput/InputMenu.svelte'; import InputMenu from './MessageInput/InputMenu.svelte';
import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte'; import FilesOverlay from './MessageInput/FilesOverlay.svelte';
@ -60,11 +71,9 @@
import Image from '../common/Image.svelte'; import Image from '../common/Image.svelte';
import XMark from '../icons/XMark.svelte'; import XMark from '../icons/XMark.svelte';
import Headphone from '../icons/Headphone.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte'; import GlobeAlt from '../icons/GlobeAlt.svelte';
import Photo from '../icons/Photo.svelte'; import Photo from '../icons/Photo.svelte';
import Wrench from '../icons/Wrench.svelte'; import Wrench from '../icons/Wrench.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import Sparkles from '../icons/Sparkles.svelte'; import Sparkles from '../icons/Sparkles.svelte';
import InputVariablesModal from './MessageInput/InputVariablesModal.svelte'; import InputVariablesModal from './MessageInput/InputVariablesModal.svelte';
@ -74,12 +83,11 @@
import Component from '../icons/Component.svelte'; import Component from '../icons/Component.svelte';
import PlusAlt from '../icons/PlusAlt.svelte'; import PlusAlt from '../icons/PlusAlt.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte'; import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
import Knobs from '../icons/Knobs.svelte'; import Knobs from '../icons/Knobs.svelte';
import ValvesModal from '../workspace/common/ValvesModal.svelte'; import ValvesModal from '../workspace/common/ValvesModal.svelte';
import PageEdit from '../icons/PageEdit.svelte';
import { goto } from '$app/navigation';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -109,6 +117,8 @@
export let webSearchEnabled = false; export let webSearchEnabled = false;
export let codeInterpreterEnabled = false; export let codeInterpreterEnabled = false;
let inputContent = null;
let showInputVariablesModal = false; let showInputVariablesModal = false;
let inputVariablesModalCallback = (variableValues) => {}; let inputVariablesModalCallback = (variableValues) => {};
let inputVariables = {}; let inputVariables = {};
@ -730,6 +740,23 @@
}); });
}; };
const createNote = async () => {
if (inputContent?.md.trim() === '' && inputContent?.html.trim() === '') {
toast.error($i18n.t('Cannot create an empty note.'));
return;
}
const res = await createNoteHandler(
dayjs().format('YYYY-MM-DD'),
inputContent?.md,
inputContent?.html
);
if (res) {
goto(`/notes/${res.id}`);
}
};
const onDragOver = (e) => { const onDragOver = (e) => {
e.preventDefault(); e.preventDefault();
@ -1195,8 +1222,9 @@
<RichTextInput <RichTextInput
bind:this={chatInputElement} bind:this={chatInputElement}
id="chat-input" id="chat-input"
onChange={(e) => { onChange={(content) => {
prompt = e.md; prompt = content.md;
inputContent = content;
command = getCommand(); command = getCommand();
}} }}
json={true} json={true}
@ -1620,13 +1648,13 @@
</div> </div>
</div> </div>
<div class="self-end flex space-x-1 mr-1 shrink-0"> <div class="self-end flex space-x-1 mr-1 shrink-0 gap-[0.5px]">
{#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))} {#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))}
<!-- {$i18n.t('Record voice')} --> <!-- {$i18n.t('Record voice')} -->
<Tooltip content={$i18n.t('Dictate')}> <Tooltip content={$i18n.t('Dictate')}>
<button <button
id="voice-input-button" id="voice-input-button"
class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center" class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 self-center"
type="button" type="button"
on:click={async () => { on:click={async () => {
try { try {
@ -1759,6 +1787,24 @@
</Tooltip> </Tooltip>
</div> </div>
{:else} {:else}
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Create note')}>
<button
id="send-message-button"
class=" text-gray-600 mr-0.5 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 self-center"
type="button"
disabled={prompt === '' && files.length === 0}
on:click={() => {
createNote();
}}
>
<PageEdit className="size-5" />
</button>
</Tooltip>
</div>
{/if}
<div class=" flex items-center"> <div class=" flex items-center">
<Tooltip content={$i18n.t('Send message')}> <Tooltip content={$i18n.t('Send message')}>
<button <button

View file

@ -0,0 +1,24 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M9 12H12M15 12H12M12 12V9M12 12V15" stroke-linecap="round" stroke-linejoin="round"
></path><path
d="M4 21.4V2.6C4 2.26863 4.26863 2 4.6 2H16.2515C16.4106 2 16.5632 2.06321 16.6757 2.17574L19.8243 5.32426C19.9368 5.43679 20 5.5894 20 5.74853V21.4C20 21.7314 19.7314 22 19.4 22H4.6C4.26863 22 4 21.7314 4 21.4Z"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M16 2V5.4C16 5.73137 16.2686 6 16.6 6H20"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -337,7 +337,7 @@
> >
<Plus className="size-3" strokeWidth="2.5" /> <Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Note')}</div> <div class=" md:ml-1 text-xs">{$i18n.t('New Note')}</div>
</button> </button>
</div> </div>
</div> </div>

View file

@ -107,7 +107,7 @@ export const downloadPdf = async (note) => {
pdf.save(`${note.title}.pdf`); pdf.save(`${note.title}.pdf`);
}; };
export const createNoteHandler = async (title: string, content?: string) => { export const createNoteHandler = async (title: string, md?: string, html?: string) => {
// $i18n.t('New Note'), // $i18n.t('New Note'),
const res = await createNewNote(localStorage.token, { const res = await createNewNote(localStorage.token, {
// YYYY-MM-DD // YYYY-MM-DD
@ -115,8 +115,8 @@ export const createNoteHandler = async (title: string, content?: string) => {
data: { data: {
content: { content: {
json: null, json: null,
html: content ?? '', html: html || md || '',
md: content ?? '' md: md || ''
} }
}, },
meta: null, meta: null,