diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index 95aea9c9fb..bbb27311f4 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -29,7 +29,7 @@ import { compressImage, copyToClipboard, splitStream } from '$lib/utils'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { uploadFile } from '$lib/apis/files'; - import { chatCompletion } from '$lib/apis/openai'; + import { chatCompletion, generateOpenAIChatCompletion } from '$lib/apis/openai'; import { config, models, settings, showSidebar, socket, user } from '$lib/stores'; @@ -121,6 +121,9 @@ let showDeleteConfirm = false; let showAccessControlModal = false; + let titleInputFocused = false; + let titleGenerating = false; + let dragged = false; let loading = false; @@ -196,6 +199,81 @@ editor.commands.setContent(note.data.content.html); }; + const generateTitleHandler = async () => { + const content = note.data.content.md; + const DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = `### Task: +Generate a concise, 3-5 word title with an emoji summarizing the content. +### Guidelines: +- The title should clearly represent the main theme or subject of the content. +- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting. +- Write the title in the chat's primary language; default to English if multilingual. +- Prioritize accuracy over excessive creativity; keep it clear and simple. +- Your entire response must consist solely of the JSON object, without any introductory or concluding text. +- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text. +- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure. +### Output: +JSON format: { "title": "your concise title here" } +### Examples: +- { "title": "📉 Stock Market Trends" }, +- { "title": "🍪 Perfect Chocolate Chip Recipe" }, +- { "title": "Evolution of Music Streaming" }, +- { "title": "Remote Work Productivity Tips" }, +- { "title": "Artificial Intelligence in Healthcare" }, +- { "title": "🎮 Video Game Development Insights" } +### Content: + +${content} +`; + + const oldTitle = JSON.parse(JSON.stringify(note.title)); + note.title = ''; + titleGenerating = true; + + const res = await generateOpenAIChatCompletion( + localStorage.token, + { + model: selectedModelId, + stream: false, + messages: [ + { + role: 'user', + content: DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE + } + ] + }, + `${WEBUI_BASE_URL}/api` + ); + if (res) { + // Step 1: Safely extract the response string + const response = res?.choices[0]?.message?.content ?? ''; + + try { + const jsonStartIndex = response.indexOf('{'); + const jsonEndIndex = response.lastIndexOf('}'); + + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1); + const parsed = JSON.parse(jsonResponse); + + if (parsed && parsed.title) { + note.title = parsed.title.trim(); + } + } + } catch (e) { + console.error('Error parsing JSON response:', e); + toast.error($i18n.t('Failed to generate title')); + } + } + + if (!note.title) { + note.title = oldTitle; + } + + titleGenerating = false; + await tick(); + changeDebounceHandler(); + }; + async function enhanceNoteHandler() { if (selectedModelId === '') { toast.error($i18n.t('Please select a model.')); @@ -776,12 +854,47 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, class="w-full text-2xl font-medium bg-transparent outline-hidden" type="text" bind:value={note.title} - placeholder={$i18n.t('Title')} - disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'} + placeholder={titleGenerating ? $i18n.t('Generating...') : $i18n.t('Title')} + disabled={(note?.user_id !== $user?.id && $user?.role !== 'admin') || + titleGenerating} required on:input={changeDebounceHandler} + on:focus={() => { + titleInputFocused = true; + }} + on:blur={(e) => { + // check if target is generate button + if (e.relatedTarget?.id === 'generate-title-button') { + return; + } + + titleInputFocused = false; + changeDebounceHandler(); + }} /> + {#if titleInputFocused && !titleGenerating} +
+ + + +
+ {/if} +
{#if editor}