From 423cafd4e75e34b487f3b5d10ec1c506f073b3da Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 15 Jul 2026 21:43:47 -0400 Subject: [PATCH] refac --- backend/open_webui/models/chats.py | 17 + backend/open_webui/routers/notes.py | 98 ++++ backend/open_webui/tools/builtin.py | 139 +++++- backend/open_webui/utils/middleware.py | 1 + backend/open_webui/utils/tools.py | 3 +- src/app.css | 2 +- src/lib/apis/notes/index.ts | 36 ++ src/lib/components/chat/Chat.svelte | 446 +++++++++++++---- src/lib/components/chat/ChatControls.svelte | 7 +- src/lib/components/chat/Messages.svelte | 2 + .../components/chat/Messages/Message.svelte | 3 + .../Messages/MultiResponseMessages.svelte | 7 +- .../chat/Messages/ResponseMessage.svelte | 17 + .../components/common/ToolCallDisplay.svelte | 25 +- src/lib/components/notes/NoteEditor.svelte | 452 +++++------------- .../components/notes/NoteEditor/Chat.svelte | 445 ----------------- .../notes/NoteEditor/Chat/Message.svelte | 103 ---- .../notes/NoteEditor/Chat/Messages.svelte | 32 -- .../notes/NoteEditor/Controls.svelte | 103 ---- src/lib/components/notes/NotePanel.svelte | 8 +- .../components/notes/Notes/NoteMenu.svelte | 15 + 21 files changed, 803 insertions(+), 1158 deletions(-) delete mode 100644 src/lib/components/notes/NoteEditor/Chat.svelte delete mode 100644 src/lib/components/notes/NoteEditor/Chat/Message.svelte delete mode 100644 src/lib/components/notes/NoteEditor/Chat/Messages.svelte delete mode 100644 src/lib/components/notes/NoteEditor/Controls.svelte diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 57342170e4..e53a074e2e 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -412,6 +412,23 @@ class ChatTable: ) return list(result.scalars().all()) + async def get_internal_chat_by_note_id( + self, note_id: str, user_id: str, db: AsyncSession | None = None + ) -> ChatModel | None: + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat) + .where( + Chat.user_id == user_id, + Chat.meta['internal'].as_boolean().is_(True), + Chat.meta['type'].as_string() == 'note', + Chat.meta['note_id'].as_string() == note_id, + ) + .order_by(Chat.created_at.asc()) + ) + chat = result.scalars().first() + return ChatModel.model_validate(chat) if chat else None + def _chat_import_form_to_chat_model(self, user_id: str, form_data: ChatImportForm) -> ChatModel: id = str(uuid.uuid4()) chat = ChatModel( diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 477558e423..465d1bf1a1 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -1,6 +1,7 @@ import json import logging from typing import Optional +from uuid import uuid4 from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status from open_webui.config import ( @@ -12,6 +13,7 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrants +from open_webui.models.chats import ChatForm, ChatResponse, Chats from open_webui.models.config import Config from open_webui.models.groups import Groups from open_webui.models.notes import ( @@ -45,6 +47,39 @@ def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optiona return {'content': {'md': md[:max_length]}} +def _note_chat_system_prompt(note_id: str) -> str: + return ( + f'You are chatting with note {note_id}. Use view_note with this note id to read the current note. ' + 'For edits, use replace_note_content for whole-note changes or replace_note_text ' + 'for targeted exact text replacement.' + ) + + +async def _normalize_note_chat_payload(chat: ChatResponse, note_id: str, db: AsyncSession) -> ChatResponse: + payload = {**(chat.chat or {})} + params = {**(payload.get('params') or {})} + changed = False + + if params.pop('note_id', None) is not None: + changed = True + + system = _note_chat_system_prompt(note_id) + if params.get('system') != system: + params['system'] = system + changed = True + + if payload.pop('system', None) is not None: + changed = True + + payload['params'] = params + if changed: + updated_chat = await Chats.update_chat_by_id(chat.id, payload, db=db, touch=False) + if updated_chat: + return updated_chat + + return chat + + ############################ # GetNotes ############################ @@ -303,6 +338,69 @@ async def get_note_by_id( ) +@router.get('/{id}/chat', response_model=ChatResponse) +async def get_note_chat_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + log.info('[note-chat] get-or-create requested note_id=%s user_id=%s', id, user.id) + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', await Config.get('user.permissions'), db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + chat = await Chats.get_internal_chat_by_note_id(note.id, user.id, db=db) + if chat: + log.info('[note-chat] reusing hidden chat note_id=%s chat_id=%s user_id=%s', note.id, chat.id, user.id) + return await _normalize_note_chat_payload(chat, note.id, db) + + meta = {'internal': True, 'type': 'note', 'note_id': note.id} + chat_id = str(uuid4()) + chat = await Chats.insert_new_chat( + chat_id, + user.id, + ChatForm( + chat={ + 'id': chat_id, + 'title': 'Chat', + 'models': [''], + 'params': {'system': _note_chat_system_prompt(note.id)}, + 'history': {'messages': {}, 'currentId': None}, + 'messages': [], + 'tags': [], + } + ), + db=db, + internal_meta=meta, + ) + if not chat: + log.error('[note-chat] failed creating hidden chat note_id=%s user_id=%s', note.id, user.id) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + log.info('[note-chat] created hidden chat note_id=%s chat_id=%s user_id=%s', note.id, chat.id, user.id) + return chat + + ############################ # UpdateNoteById ############################ diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index c59ea54aaa..030164f902 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -49,6 +49,8 @@ from open_webui.routers.memories import ( add_memory as _add_memory, ) from open_webui.routers.retrieval import search_web as _search_web +from open_webui.events import EVENTS, publish_event +from open_webui.socket.main import sio from open_webui.utils.sanitize import sanitize_code log = logging.getLogger(__name__) @@ -56,6 +58,33 @@ log = logging.getLogger(__name__) MAX_KNOWLEDGE_BASE_SEARCH_ITEMS = 10_000 +async def _has_write_access_to_note(note, user_id: str) -> bool: + if note.user_id == user_id: + return True + + from open_webui.models.access_grants import AccessGrants + + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + return await AccessGrants.has_access( + user_id=user_id, + resource_type='note', + resource_id=note.id, + permission='write', + user_group_ids=set(user_group_ids), + ) + + +async def _emit_note_updated(request: Request, user: dict, note) -> None: + await sio.emit('note-events', note.model_dump(), to=f'note:{note.id}') + await publish_event( + request, + EVENTS.NOTE_UPDATED, + actor=user, + subject_id=note.id, + data={'title': note.title}, + ) + + async def _has_read_access_to_file( file, user_id: str, @@ -1155,23 +1184,21 @@ async def replace_note_content( if not note: return json.dumps({'error': 'Note not found'}) - # Check write permission user_id = __user__.get('id') - user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] - - from open_webui.models.access_grants import AccessGrants - - if note.user_id != user_id and not await AccessGrants.has_access( - user_id=user_id, - resource_type='note', - resource_id=note.id, - permission='write', - user_group_ids=set(user_group_ids), - ): + if not await _has_write_access_to_note(note, user_id): return json.dumps({'error': 'Write access denied'}) - # Build update form - update_data = {'data': {'content': {'md': content}}} + update_data = { + 'data': { + **(note.data or {}), + 'content': { + **((note.data or {}).get('content') or {}), + 'json': None, + 'html': '', + 'md': content, + }, + } + } if title: update_data['title'] = title @@ -1181,6 +1208,8 @@ async def replace_note_content( if not updated_note: return json.dumps({'error': 'Failed to update note'}) + await _emit_note_updated(__request__, __user__, updated_note) + return json.dumps( { 'status': 'success', @@ -1195,6 +1224,88 @@ async def replace_note_content( return json.dumps({'error': str(e)}) +async def replace_note_text( + note_id: str, + target_text: str, + replacement_text: str, + title: Optional[str] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Replace an exact text span in a note when it occurs exactly once. + + :param note_id: The ID of the note to update + :param target_text: Exact text to replace + :param replacement_text: Replacement text + :param title: Optional new title for the note + :return: JSON with success status or a clear match-count error + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.notes import NoteUpdateForm + + note = await Notes.get_note_by_id(note_id) + if not note: + return json.dumps({'error': 'Note not found'}) + + user_id = __user__.get('id') + if not await _has_write_access_to_note(note, user_id): + return json.dumps({'error': 'Write access denied'}) + + if target_text == '': + return json.dumps({'error': 'target_text must not be empty'}) + + content = ((note.data or {}).get('content') or {}).get('md') or '' + match_count = content.count(target_text) + if match_count != 1: + return json.dumps( + { + 'error': 'target_text must occur exactly once', + 'match_count': match_count, + }, + ensure_ascii=False, + ) + + update_data = { + 'data': { + **(note.data or {}), + 'content': { + **((note.data or {}).get('content') or {}), + 'json': None, + 'html': '', + 'md': content.replace(target_text, replacement_text, 1), + }, + } + } + if title: + update_data['title'] = title + + updated_note = await Notes.update_note_by_id(note_id, NoteUpdateForm(**update_data)) + if not updated_note: + return json.dumps({'error': 'Failed to update note'}) + + await _emit_note_updated(__request__, __user__, updated_note) + + return json.dumps( + { + 'status': 'success', + 'id': updated_note.id, + 'title': updated_note.title, + 'updated_at': updated_note.updated_at, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'replace_note_text error: {e}') + return json.dumps({'error': str(e)}) + + # ============================================================================= # CHATS TOOLS # ============================================================================= diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index b3a5258395..c4e650f430 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1881,6 +1881,7 @@ def apply_params_to_form_data(form_data, model): 'reasoning_tags': list, 'compact_token_threshold': int, 'system': str, + 'note_id': str, } for key in list(params.keys()): diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 93f6d11ffc..9e692eb857 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -72,6 +72,7 @@ from open_webui.tools.builtin import ( read_memory_path, replace_memory_content, replace_note_content, + replace_note_text, search_calendar_events, search_channel_messages, search_channels, @@ -635,7 +636,7 @@ async def get_builtin_tools( # Notes tools - search, view, create, and update user's notes if is_builtin_tool_enabled('notes') and config.get('notes.enable') and await has_user_permission('notes'): - builtin_functions.extend([search_notes, view_note, write_note, replace_note_content]) + builtin_functions.extend([search_notes, view_note, write_note, replace_note_content, replace_note_text]) # Channels tools - search channels and messages if is_builtin_tool_enabled('channels') and config.get('channels.enable') and await has_user_permission('channels'): diff --git a/src/app.css b/src/app.css index 8964f6506f..527e92046d 100644 --- a/src/app.css +++ b/src/app.css @@ -122,7 +122,7 @@ textarea::-webkit-scrollbar-corner { } .markdown-prose { - @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-normal prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply prose dark:prose-invert max-w-none break-words font-normal leading-relaxed prose-p:my-0 prose-p:font-normal prose-p:leading-relaxed prose-headings:my-1 prose-headings:font-normal prose-headings:leading-snug prose-strong:font-medium prose-code:before:content-none prose-code:after:content-none prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 prose-li:font-normal prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-blockquote:font-normal prose-hr:my-4 prose-img:my-1 [&>:first-child]:mt-0 [&>:last-child]:mb-0; } .markdown-prose-sm { diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index 80f0413bbc..b5603beeba 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -218,6 +218,42 @@ export const getNoteById = async (token: string, id: string) => { return res; }; +export const getNoteChatById = async (token: string, id: string) => { + let error = null; + const url = `${WEBUI_API_BASE_URL}/notes/${id}/chat`; + + console.info('[note-chat] fetching linked chat', { noteId: id, url }); + + const res = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + console.info('[note-chat] linked chat response', { + noteId: id, + status: res.status, + ok: res.ok + }); + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error('[note-chat] linked chat request failed', { noteId: id, error: err }); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const updateNoteById = async (token: string, id: string, note: NoteItem) => { let error = null; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index a271cdf923..bd6600357f 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -115,10 +115,24 @@ import Tooltip from '../common/Tooltip.svelte'; import Sidebar from '../icons/Sidebar.svelte'; import Image from '../common/Image.svelte'; + import XMark from '../icons/XMark.svelte'; export let chatIdProp = ''; + export let embedded = false; + export let embeddedTitle = ''; + export let initialFiles = []; + export let selectedText = ''; + export let onInsertToNote: ((content: string) => void) | null = null; + export let onCloseEmbedded: (() => void) | null = null; let loading = true; + $: chatContainerId = embedded ? 'note-chat-container' : 'chat-container'; + const embeddedSuggestedPrompts = [ + 'Enhance this note and update it.', + 'Summarize this note.', + 'Extract action items from this note.', + 'Rewrite the selected text.' + ]; const eventTarget = new EventTarget(); let controlPane: Pane | undefined; @@ -155,6 +169,30 @@ let serverContextUsage = null; let contextUsage = null; + const getAvailableModelIds = () => + $models.filter((m) => !(m?.info?.meta?.hidden ?? false)).map((m) => m.id); + const getDefaultModelIds = () => + $config?.default_models ? $config.default_models.split(',') : []; + const normalizeSelectedModels = (modelIds = []) => { + const availableModels = getAvailableModelIds(); + const defaultModels = getDefaultModelIds(); + let normalized = (modelIds ?? []).filter( + (modelId) => modelId && availableModels.includes(modelId) + ); + + if (normalized.length === 0 && $settings?.models?.length) { + normalized = $settings.models.filter((modelId) => availableModels.includes(modelId)); + } + if (normalized.length === 0 && defaultModels.length > 0) { + normalized = defaultModels.filter((modelId) => availableModels.includes(modelId)); + } + if (normalized.length === 0) { + normalized = availableModels.length > 0 ? [availableModels[0]] : ['']; + } + + return normalized; + }; + const estimateTokens = (value) => { if (value === null || value === undefined || value === '') { return 0; @@ -240,6 +278,10 @@ }; $: contextUsage = getContextUsage() ?? serverContextUsage; + $: embeddedHeaderTitle = + embeddedTitle || + ($chatTitle && !$chatTitle.startsWith('Chat:') ? $chatTitle : '') || + $i18n.t('Chat'); let selectedToolIds = []; let selectedSkillIds = []; @@ -320,8 +362,56 @@ let chatFiles = []; let files = []; let params = {}; + let appliedInitialFilesKey = ''; + let loadedChatIdProp = ''; - $: if (chatIdProp) { + const fileKey = (file) => `${file?.type ?? ''}:${file?.id ?? file?.url ?? file?.name ?? ''}`; + const mergeFiles = (current, incoming) => { + const seen = new Set(); + return [...(incoming ?? []), ...(current ?? [])].filter((file) => { + const key = fileKey(file); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + }; + const applyInitialFiles = () => { + if (!embedded || !initialFiles?.length) return; + + const key = JSON.stringify(initialFiles.map(fileKey)); + if (key === appliedInitialFilesKey) return; + + files = mergeFiles(files, initialFiles); + chatFiles = mergeFiles(chatFiles, initialFiles); + appliedInitialFilesKey = key; + }; + const withSelectedText = (text: string) => + embedded && selectedText?.trim() + ? `${text}\n\nSelected note text:\n${selectedText.trim()}` + : text; + const submitEmbeddedPrompt = async (text: string) => { + await tick(); + await submitHandler(withSelectedText(text)); + }; + const noteChatDebug = (message: string, data: Record = {}) => { + if (!embedded) return; + console.info('[note-chat]', message, { + chatIdProp, + activeChatId: $chatId, + loading, + ...data + }); + }; + + $: if (embedded && !loading) { + applyInitialFiles(); + } + + $: if (chatIdProp && chatIdProp !== loadedChatIdProp) { + noteChatDebug('chatIdProp changed; loading linked chat', { + previousChatIdProp: loadedChatIdProp + }); + loadedChatIdProp = chatIdProp; navigateHandler(); } @@ -332,9 +422,11 @@ } const navigateHandler = async () => { + noteChatDebug('navigateHandler start'); // Mark the outgoing chat as read before loading the new one. // $chatId still holds the previous chat here — loadChat() updates it. if ($chatId && $chatId !== chatIdProp && !$temporaryChatEnabled) { + noteChatDebug('marking outgoing chat read', { outgoingChatId: $chatId }); updateLastReadAt($chatId); } @@ -356,9 +448,12 @@ `chat-input${chatIdProp ? `-${chatIdProp}` : ''}` ); - if (chatIdProp && (await loadChat())) { + const loaded = chatIdProp ? await loadChat() : false; + noteChatDebug('loadChat completed inside navigateHandler', { loaded }); + if (loaded) { await tick(); loading = false; + noteChatDebug('embedded chat loading false'); window.setTimeout(() => scrollToBottom(), 0); await tick(); @@ -396,8 +491,14 @@ const chatInput = document.getElementById('chat-input'); chatInput?.focus(); - } else { + } else if (!embedded) { await goto('/'); + } else { + loading = false; + console.warn('[note-chat] embedded load failed; clearing spinner', { + chatIdProp, + activeChatId: $chatId + }); } }; @@ -1548,7 +1649,7 @@ await showCallOverlay.set(false); await showArtifacts.set(false); - if ($page.url.pathname.includes('/c/')) { + if (!embedded && $page.url.pathname.includes('/c/')) { window.history.replaceState(history.state, '', `/`); } @@ -1676,27 +1777,53 @@ }; const loadChat = async () => { + noteChatDebug('loadChat start'); // chatIdProp is empty for chats started from the home page (URL set via replaceState) chatId.set(chatIdProp || $chatId); + noteChatDebug('loadChat set active chat id'); if ($temporaryChatEnabled) { + noteChatDebug('loadChat disabling temporary chat'); temporaryChatEnabled.set(false); } chat = await getChatById(localStorage.token, $chatId).catch(async (error) => { - await goto('/'); + console.error('[note-chat] getChatById failed', { + chatIdProp, + activeChatId: $chatId, + error + }); + if (!embedded) { + await goto('/'); + } return null; }); + noteChatDebug('getChatById completed', { + found: !!chat, + chatId: chat?.id, + hasChatPayload: !!chat?.chat, + title: chat?.title + }); if (chat) { tags = await getTagsById(localStorage.token, $chatId).catch(async (error) => { + console.warn('[note-chat] getTagsById failed; continuing without tags', { + chatIdProp, + activeChatId: $chatId, + error + }); return []; }); + noteChatDebug('getTagsById completed', { tagCount: tags?.length ?? 0 }); const chatContent = chat.chat; if (chatContent) { - console.log(chatContent); + noteChatDebug('chat payload found', { + models: chatContent?.models, + hasHistory: !!chatContent?.history, + messageCount: Object.keys(chatContent?.history?.messages ?? {}).length + }); selectedModels = (chatContent?.models ?? undefined) !== undefined @@ -1706,6 +1833,13 @@ if (!($user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true))) { selectedModels = selectedModels.length > 0 ? [selectedModels[0]] : ['']; } + if ( + selectedModels.length === 0 || + (selectedModels.length === 1 && selectedModels[0] === '') + ) { + selectedModels = normalizeSelectedModels(selectedModels); + noteChatDebug('normalized empty selected models after load', { selectedModels }); + } oldSelectedModelIds = structuredClone(selectedModels); @@ -1721,6 +1855,7 @@ chatTitle.set(chatContent.title); params = structuredClone(chatContent?.params ?? {}); + delete params.note_id; chatFiles = structuredClone(chatContent?.files ?? []); // Load tasks from chat-level DB field @@ -1748,13 +1883,25 @@ // If the response is already done, remaining tasks are just background // work (follow-ups, title gen) that shouldn't block the input. const activeTaskIds = taskIds; + const currentMessage = history.currentId ? history.messages[history.currentId] : null; const pendingTaskIds = await getTaskIdsByChatId(localStorage.token, $chatId) .then((res) => res?.task_ids ?? []) - .catch(() => []); + .catch((error) => { + console.warn('[note-chat] getTaskIdsByChatId failed; continuing without tasks', { + chatIdProp, + activeChatId: $chatId, + error + }); + return []; + }); + noteChatDebug('task reconciliation completed', { + pendingTaskCount: pendingTaskIds.length, + hasCurrentMessage: !!currentMessage + }); if (taskIds !== activeTaskIds) { + noteChatDebug('task ids changed during load; aborting stale load'); return; } - const currentMessage = history.currentId ? history.messages[history.currentId] : null; const responseComplete = currentMessage?.role === 'assistant' && currentMessage?.done; if (pendingTaskIds.length > 0 && !responseComplete) { @@ -1771,9 +1918,18 @@ return true; } else { + console.warn('[note-chat] chat response missing chat payload', { + chatIdProp, + activeChatId: $chatId, + chat + }); return null; } } + console.warn('[note-chat] no chat returned from getChatById', { + chatIdProp, + activeChatId: $chatId + }); }; const scrollToBottom = async (behavior = 'auto') => { @@ -2298,8 +2454,10 @@ ); if (result?.id) { - await goto(`/c/${result.id}`); - await refreshChatList(localStorage.token, { refreshPinned: true }); + if (!embedded) { + await goto(`/c/${result.id}`); + await refreshChatList(localStorage.token, { refreshPinned: true }); + } toast.success($i18n.t('Chat forked'), { id: toastId }); } else { toast.error($i18n.t('Failed to fork chat'), { id: toastId }); @@ -2857,7 +3015,7 @@ // and causing spurious toast notifications / state duplication). if (res.chat_id && $chatId !== res.chat_id && $chatId === _chatId) { await chatId.set(res.chat_id); - if (!$temporaryChatEnabled) { + if (!$temporaryChatEnabled && !embedded) { window.history.replaceState(history.state, '', `/c/${res.chat_id}`); await refreshChatList(localStorage.token); @@ -3134,11 +3292,15 @@ _chatId = chat.id; await chatId.set(_chatId); - window.history.replaceState(history.state, '', `/c/${_chatId}`); + if (!embedded) { + window.history.replaceState(history.state, '', `/c/${_chatId}`); + } await tick(); - await refreshChatList(localStorage.token); + if (!embedded) { + await refreshChatList(localStorage.token); + } selectedFolder.set(null); } else { @@ -3340,14 +3502,17 @@ />
{#if !loading}
- {#if $selectedFolder && $selectedFolder?.meta?.background_image_url} + {#if !embedded && $selectedFolder && $selectedFolder?.meta?.background_image_url}
- {:else if $settings?.backgroundImageUrl ?? $config?.license_metadata?.background_image_url ?? null} + {:else if !embedded && ($settings?.backgroundImageUrl ?? $config?.license_metadata?.background_image_url ?? null)}
- { - try { - if (!history?.currentId || !Object.keys(history.messages).length) { - toast.error($i18n.t('No conversation to save')); - return; + {#if embedded} +
+
+ {embeddedHeaderTitle} +
+ + + +
+ {:else} + m.role === 'user')?.content ?? $i18n.t('New Chat'); + }} + {history} + title={$chatTitle} + shareEnabled={!!history.currentId} + {initNewChat} + scrollToTop={!isNearTop ? scrollToTop : null} + {archiveChatHandler} + {deleteChatHandler} + {moveChatHandler} + onSaveTempChat={async () => { + try { + if (!history?.currentId || !Object.keys(history.messages).length) { + toast.error($i18n.t('No conversation to save')); + return; + } + const messages = createMessagesList(history, history.currentId); + const title = + messages.find((m) => m.role === 'user')?.content ?? $i18n.t('New Chat'); - const savedChat = await createNewChat( - localStorage.token, - { - id: uuidv4(), - title: title.length > 50 ? `${title.slice(0, 50)}...` : title, - models: selectedModels, - params: params, - history: history, - messages: messages, - timestamp: Date.now() - }, - null - ); + const savedChat = await createNewChat( + localStorage.token, + { + id: uuidv4(), + title: title.length > 50 ? `${title.slice(0, 50)}...` : title, + models: selectedModels, + params: params, + history: history, + messages: messages, + timestamp: Date.now() + }, + null + ); - if (savedChat) { - temporaryChatEnabled.set(false); - chatId.set(savedChat.id); - await refreshChatList(localStorage.token); + if (savedChat) { + temporaryChatEnabled.set(false); + chatId.set(savedChat.id); + await refreshChatList(localStorage.token); - await goto(`/c/${savedChat.id}`); - toast.success($i18n.t('Conversation saved successfully')); + await goto(`/c/${savedChat.id}`); + toast.success($i18n.t('Conversation saved successfully')); + } + } catch (error) { + console.error('Error saving conversation:', error); + toast.error($i18n.t('Failed to save conversation')); } - } catch (error) { - console.error('Error saving conversation:', error); - toast.error($i18n.t('Failed to save conversation')); - } - }} - /> + }} + /> + {/if}
{#if ($settings?.landingPageMode === 'chat' && !$selectedFolder) || createMessagesList(history, history.currentId).length > 0} @@ -3458,6 +3643,7 @@ }} bind:selectedModels {atSelectedModel} + className={embedded ? 'h-full flex pt-4' : 'h-full flex pt-18'} {sendMessage} {showMessage} {submitMessage} @@ -3466,9 +3652,10 @@ {mergeResponses} {chatActionHandler} {addMessages} - topPadding={true} + topPadding={!embedded} bottomPadding={files.length > 0} {onSelect} + {onInsertToNote} />
@@ -3557,7 +3744,7 @@ if (e.detail || files.length > 0) { await tick(); - submitHandler(e.detail); + submitHandler(withSelectedText(e.detail)); } }} /> @@ -3569,6 +3756,68 @@
{/if} + {:else if embedded} +
+
+
+
+ {$i18n.t('Suggested prompts')} +
+
+ {#each embeddedSuggestedPrompts as suggestion} + + {/each} +
+
+
+
+ { + clearDraft($chatId); + if (e.detail || files.length > 0) { + await tick(); + submitHandler(withSelectedText(e.detail)); + } + }} + /> +
+
{:else}
0) { await tick(); - submitHandler(e.detail); + submitHandler(withSelectedText(e.detail)); } }} /> @@ -3612,28 +3861,31 @@
- { - const model = $models.find((m) => m.id === e); - if (model) { - return [...a, model]; - } - return a; - }, [])} - submitPrompt={submitHandler} - {stopResponse} - {showMessage} - {eventTarget} - {codeInterpreterEnabled} - /> + {#if !embedded} + { + const model = $models.find((m) => m.id === e); + if (model) { + return [...a, model]; + } + return a; + }, [])} + submitPrompt={submitHandler} + {stopResponse} + {showMessage} + {eventTarget} + {codeInterpreterEnabled} + containerId={chatContainerId} + /> + {/if}
{:else if loading} diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index 8d2a05bcca..ef7592e4dc 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -55,6 +55,7 @@ export let codeInterpreterEnabled = false; export let pane: Pane | null = null; + export let containerId = 'chat-container'; let largeScreen = false; let dragged = false; @@ -159,7 +160,7 @@ export const openPane = () => { if (parseInt(localStorage?.chatControlsSize)) { - const container = document.getElementById('chat-container'); + const container = document.getElementById(containerId); let size = Math.floor( (parseInt(localStorage?.chatControlsSize) / container.clientWidth) * 100 ); @@ -218,7 +219,7 @@ paneReady = true; }, 0); - const container = document.getElementById('chat-container') as HTMLElement; + const container = document.getElementById(containerId) as HTMLElement; if (!container) return; minSize = Math.floor((350 / container.clientWidth) * 100); @@ -407,7 +408,7 @@ if (size < minSize) { localStorage.chatControlsSize = 0; } else { - const container = document.getElementById('chat-container'); + const container = document.getElementById(containerId); localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth); } } diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index fadabff79a..27afd1ec21 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -49,6 +49,7 @@ export let autoScroll; export let onSelect = (e) => {}; + export let onInsertToNote: ((content: string) => void) | null = null; export let messagesCount: number | null = 8; let messagesLoading = false; @@ -542,6 +543,7 @@ {readOnly} {editCodeBlock} {topPadding} + {onInsertToNote} /> {/each} diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte index 8c9a33d77f..447a5b0554 100644 --- a/src/lib/components/chat/Messages/Message.svelte +++ b/src/lib/components/chat/Messages/Message.svelte @@ -43,6 +43,7 @@ export let readOnly = false; export let editCodeBlock = true; export let topPadding = false; + export let onInsertToNote: ((content: string) => void) | null = null;
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} {/key} {/if} diff --git a/src/lib/components/chat/Messages/MultiResponseMessages.svelte b/src/lib/components/chat/Messages/MultiResponseMessages.svelte index 21cea0c93c..97152cf5ac 100644 --- a/src/lib/components/chat/Messages/MultiResponseMessages.svelte +++ b/src/lib/components/chat/Messages/MultiResponseMessages.svelte @@ -49,6 +49,7 @@ export let triggerScroll: Function; export let topPadding = false; + export let onInsertToNote: ((content: string) => void) | null = null; const dispatch = createEventDispatcher(); @@ -320,6 +321,7 @@ {addMessages} {readOnly} {topPadding} + {onInsertToNote} /> {/if} {/key} @@ -377,6 +379,7 @@ {readOnly} {editCodeBlock} {topPadding} + {onInsertToNote} /> {/if} {/key} @@ -411,7 +414,9 @@
{#if message.timestamp} -
+
void) | null = null; let citationsElement: HTMLDivElement; @@ -1051,6 +1052,22 @@ + {#if onInsertToNote && visibleResponseContent} + + + + {/if} + {#if !readOnly && ($user?.role === 'admin' || ($user?.permissions?.chat?.tts ?? true))}
@@ -174,9 +163,9 @@
{#if open} - + {:else} - + {/if}
@@ -184,7 +173,7 @@ {#if open}
-
+
{#if args}
@@ -210,7 +199,7 @@ {:else}
{formatJSONString(
+										class="text-xs text-gray-600 dark:text-gray-300 whitespace-pre font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-2 overflow-x-auto">{formatJSONString(
 											args
 										)}
@@ -229,7 +218,7 @@
{#if typeof parsedResult === 'object' && parsedResult !== null}
{JSON.stringify(
+										class="text-xs text-gray-600 dark:text-gray-300 whitespace-pre font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-2 overflow-x-auto">{JSON.stringify(
 											parsedResult,
 											null,
 											2
diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte
index 6d0a569eb2..8a1e593708 100644
--- a/src/lib/components/notes/NoteEditor.svelte
+++ b/src/lib/components/notes/NoteEditor.svelte
@@ -23,10 +23,10 @@
 
 	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 
-	import { compressImage, copyToClipboard, splitStream, convertHeicToJpeg } from '$lib/utils';
-	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
+	import { compressImage, copyToClipboard, convertHeicToJpeg } from '$lib/utils';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { getFileById, uploadFile } from '$lib/apis/files';
-	import { chatCompletion, generateOpenAIChatCompletion } from '$lib/apis/openai';
+	import { generateOpenAIChatCompletion } from '$lib/apis/openai';
 
 	import {
 		config,
@@ -42,8 +42,7 @@
 
 	import { downloadPdf } from './utils';
 
-	import Controls from './NoteEditor/Controls.svelte';
-	import Chat from './NoteEditor/Chat.svelte';
+	import Chat from '$lib/components/chat/Chat.svelte';
 
 	import NotePanel from '$lib/components/notes/NotePanel.svelte';
 	import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
@@ -65,6 +64,7 @@
 	import {
 		deleteNoteById,
 		getNoteById,
+		getNoteChatById,
 		updateNoteById,
 		updateNoteAccessGrants,
 		toggleNotePinnedStatusById,
@@ -76,28 +76,18 @@
 	import Mic from '../icons/Mic.svelte';
 	import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
-	import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
 
-	import Calendar from '../icons/Calendar.svelte';
-	import Users from '../icons/Users.svelte';
 	import AccessButton from '../common/AccessButton.svelte';
 
-	import Image from '../common/Image.svelte';
-	import FileItem from '../common/FileItem.svelte';
 	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
 	import RecordMenu from './RecordMenu.svelte';
 	import NoteMenu from './Notes/NoteMenu.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import Sparkles from '../icons/Sparkles.svelte';
-	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';
 	import Sidebar from '../icons/Sidebar.svelte';
-	import ArrowRight from '../icons/ArrowRight.svelte';
-	import Cog6 from '../icons/Cog6.svelte';
-	import AiMenu from './AIMenu.svelte';
 	import AdjustmentsHorizontalOutline from '../icons/AdjustmentsHorizontalOutline.svelte';
 
 	export let id: null | string = null;
@@ -131,7 +121,6 @@
 		);
 
 	let files = [];
-	let messages = [];
 
 	let wordCount = 0;
 	let charCount = 0;
@@ -142,8 +131,9 @@
 	let recording = false;
 	let displayMediaRecord = false;
 
-	let showPanel = false;
-	let selectedPanel = 'chat';
+	let showNoteChat = false;
+	let noteChatId = null;
+	let noteChatLoading = false;
 
 	let selectedContent = null;
 
@@ -157,11 +147,6 @@
 	let dragged = false;
 	let loading = false;
 
-	let editing = false;
-	let streaming = false;
-
-	let stopResponseFlag = false;
-
 	let inputElement = null;
 
 	// Computed HTML for editor: fall back to markdown if HTML is missing
@@ -176,8 +161,6 @@
 			return null;
 		});
 
-		messages = [];
-
 		if (res) {
 			note = res;
 			if (!Array.isArray(note?.access_grants)) {
@@ -249,11 +232,6 @@
 		return false;
 	}
 
-	const onEdited = async () => {
-		if (!editor) return;
-		editor.commands.setContent(note.data.content.html);
-	};
-
 	const generateTitleHandler = async () => {
 		const content = note.data.content.md;
 		const DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = `### Task:
@@ -328,34 +306,6 @@ ${content}
 		changeDebounceHandler();
 	};
 
-	async function enhanceNoteHandler() {
-		if (selectedModelId === '') {
-			toast.error($i18n.t('Please select a model.'));
-			return;
-		}
-
-		const model = $models
-			.filter((model) => model.id === selectedModelId && !(model?.info?.meta?.hidden ?? false))
-			.find((model) => model.id === selectedModelId);
-
-		if (!model) {
-			selectedModelId = '';
-			return;
-		}
-
-		editing = true;
-		await enhanceCompletionHandler(model);
-		editing = false;
-
-		onEdited();
-		versionIdx = null;
-	}
-
-	const stopResponseHandler = async () => {
-		stopResponseFlag = true;
-		console.log('stopResponse', stopResponseFlag);
-	};
-
 	function setContentByVersion(versionIdx) {
 		if (!note.data.versions?.length) return;
 		let idx = versionIdx;
@@ -433,13 +383,6 @@ ${content}
 
 		files = [...files, fileItem];
 
-		// open the settings panel if it is not open
-		selectedPanel = 'settings';
-
-		if (!showPanel) {
-			showPanel = true;
-		}
-
 		try {
 			// If the file is an audio file, provide the language for STT.
 			let metadata = null;
@@ -597,6 +540,51 @@ ${content}
 		});
 	};
 
+	const noteChatFileKey = (file) =>
+		`${file?.type ?? ''}:${file?.id ?? file?.url ?? file?.name ?? ''}`;
+	const getNoteChatFiles = () => {
+		if (!note) return [];
+
+		const seen = new Set();
+		return (note?.data?.files ?? files ?? []).filter((file) => {
+			const key = noteChatFileKey(file);
+			if (seen.has(key)) return false;
+			seen.add(key);
+			return true;
+		});
+	};
+
+	const openNoteChat = async () => {
+		console.info('[note-chat] open requested', {
+			noteId: note?.id,
+			alreadyLoading: noteChatLoading,
+			currentChatId: noteChatId,
+			sidebarOpen: showNoteChat
+		});
+		if (!note?.id || noteChatLoading) return;
+
+		noteChatLoading = true;
+		const chat = await getNoteChatById(localStorage.token, note.id).catch((error) => {
+			console.error('[note-chat] open failed', { noteId: note?.id, error });
+			toast.error(`${error}`);
+			return null;
+		});
+		noteChatLoading = false;
+
+		if (chat?.id) {
+			console.info('[note-chat] open resolved', {
+				noteId: note.id,
+				chatId: chat.id,
+				title: chat.title,
+				hasChatPayload: !!chat.chat
+			});
+			noteChatId = chat.id;
+			showNoteChat = true;
+		} else {
+			console.warn('[note-chat] open returned no chat id', { noteId: note.id, chat });
+		}
+	};
+
 	const downloadHandler = async (type) => {
 		console.log('downloadHandler', type);
 		if (type === 'txt') {
@@ -629,113 +617,6 @@ ${content}
 		}
 	};
 
-	const scrollToBottom = () => {
-		const element = document.getElementById('note-content-container');
-
-		if (element) {
-			element.scrollTop = element?.scrollHeight;
-		}
-	};
-
-	const enhanceCompletionHandler = async (model) => {
-		stopResponseFlag = false;
-		let enhancedContent = {
-			json: null,
-			html: '',
-			md: ''
-		};
-
-		const systemPrompt = `Enhance existing notes using additional context provided from audio transcription or uploaded file content in the content's primary language. Your task is to make the notes more useful and comprehensive by incorporating relevant information from the provided context.
-
-Input will be provided within  and  XML tags, providing a structure for the existing notes and context respectively.
-
-# Output Format
-
-Provide the enhanced notes in markdown format. Use markdown syntax for headings, lists, task lists ([ ]) where tasks or checklists are strongly implied, and emphasis to improve clarity and presentation. Ensure that all integrated content from the context is accurately reflected. Return only the markdown formatted note.
-`;
-
-		const [res, controller] = await chatCompletion(
-			localStorage.token,
-			{
-				model: model.id,
-				stream: true,
-				messages: [
-					{
-						role: 'system',
-						content: systemPrompt
-					},
-					{
-						role: 'user',
-						content:
-							`${note.data.content.md}` +
-							(files && files.length > 0
-								? `\n${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}`
-								: '')
-					}
-				]
-			},
-			`${WEBUI_BASE_URL}/api`
-		);
-
-		await tick();
-
-		streaming = true;
-
-		if (res && res.ok) {
-			const reader = res.body
-				.pipeThrough(new TextDecoderStream())
-				.pipeThrough(splitStream('\n'))
-				.getReader();
-
-			while (true) {
-				const { value, done } = await reader.read();
-				if (done || stopResponseFlag) {
-					if (stopResponseFlag) {
-						controller.abort('User: Stop Response');
-					}
-
-					editing = false;
-					streaming = false;
-					break;
-				}
-
-				try {
-					let lines = value.split('\n');
-
-					for (const line of lines) {
-						if (line !== '') {
-							console.log(line);
-							if (line === 'data: [DONE]') {
-								console.log(line);
-							} else {
-								let data = JSON.parse(line.replace(/^data: /, ''));
-								console.log(data);
-
-								if (data.choices && data.choices.length > 0) {
-									const choice = data.choices[0];
-									if (choice.delta && choice.delta.content) {
-										enhancedContent.md += choice.delta.content;
-										enhancedContent.html = marked.parse(enhancedContent.md);
-
-										note.data.content.md = enhancedContent.md;
-										note.data.content.html = enhancedContent.html;
-										note.data.content.json = null;
-
-										scrollToBottom();
-									}
-								}
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-				}
-			}
-		}
-
-		streaming = false;
-	};
-
 	const onDragOver = (e) => {
 		e.preventDefault();
 
@@ -800,6 +681,18 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 			note.data.files = files;
 		}
 
+		if (_note.data?.content) {
+			note.data.content = {
+				...note.data.content,
+				..._note.data.content
+			};
+			if (editor) {
+				editor.commands.setContent(
+					_note.data.content.html || marked.parse(_note.data.content.md ?? '')
+				);
+			}
+		}
+
 		if (_note.title && _note.title) {
 			note.title = _note.title;
 		}
@@ -1025,44 +918,19 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 											
{/if} - - - - - - - - {/if} + + + + {#if note?.write_access} { @@ -1150,6 +1018,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, onDelete={() => { showDeleteConfirm = true; }} + onChat={openNoteChat} isPinned={$pinnedNotes.some((n) => n.id === note.id)} onPin={async () => { await toggleNotePinnedStatusById(localStorage.token, note.id); @@ -1239,14 +1108,6 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, class=" flex-1 w-full h-full overflow-auto px-2.5 relative" id="note-content-container" > - {#if editing} -
- {/if} - { const { from, to } = editor.state.selection; const selectedText = editor.state.doc.textBetween(from, to, ' '); @@ -1349,126 +1210,47 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
{/if}
-
-
- {#if recording} -
- { - recording = false; - displayMediaRecord = false; - }} - onConfirm={(data) => { - if (data?.file) { - uploadFileHandler(data?.file); - } - - recording = false; - displayMediaRecord = false; - }} - /> -
- {:else} -
- - {#if editing} - - {:else} - { - enhanceNoteHandler(); - }} - onChat={() => { - showPanel = true; - selectedPanel = 'chat'; - }} - > -
- -
-
- {/if} -
-
- {/if} -
-
- - - {#if selectedPanel === 'chat'} - { - insertNoteVersion(note); - }} - scrollToBottomHandler={scrollToBottom} - /> - {:else if selectedPanel === 'settings'} - { - files = updatedFiles; - note.data.files = files.length > 0 ? files : null; - - if (editor) { - editor.storage.files = files; - const fileIds = new Set(files.map((file) => file.id)); - const ranges = []; - - editor.state.doc.descendants((node, pos) => { - const src = node.attrs.src; - if ( - node.type.name === 'image' && - src?.startsWith('data://') && - !fileIds.has(src.slice('data://'.length)) - ) { - ranges.push([pos, pos + node.nodeSize]); + {#if recording} +
+
+ { + recording = false; + displayMediaRecord = false; + }} + onConfirm={(data) => { + if (data?.file) { + uploadFileHandler(data?.file); } - }); - if (ranges.length > 0) { - let transaction = editor.state.tr; - ranges.reverse().forEach(([from, to]) => { - transaction = transaction.delete(from, to); - }); - editor.view.dispatch(transaction); - } - } - - changeDebounceHandler(); + recording = false; + displayMediaRecord = false; + }} + /> +
+
+ {/if} + + + {#if noteChatLoading} +
+ +
+ {:else if noteChatId} + { + showNoteChat = false; }} /> {/if} diff --git a/src/lib/components/notes/NoteEditor/Chat.svelte b/src/lib/components/notes/NoteEditor/Chat.svelte deleted file mode 100644 index 64fda60d91..0000000000 --- a/src/lib/components/notes/NoteEditor/Chat.svelte +++ /dev/null @@ -1,445 +0,0 @@ - - -
-
- -
- -
-
- {$i18n.t('Chat')} -
- -
- - {$i18n.t('Experimental')} - -
-
-
- -
-
-
-
- - -
- {#if selectedContent} -
-
-
- {selectedContent?.text} -
-
-
- {/if} - - -
-
- - - -
- - - - -
-
-
-
-
-
-
diff --git a/src/lib/components/notes/NoteEditor/Chat/Message.svelte b/src/lib/components/notes/NoteEditor/Chat/Message.svelte deleted file mode 100644 index d9a352a630..0000000000 --- a/src/lib/components/notes/NoteEditor/Chat/Message.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - -
-
-
- {$i18n.t(message.role)} -
- -
- - - - - - - - - - - -
-
- -
- - - - {#if !(message?.done ?? true) && message?.content === ''} -
- -
- {:else if message?.edit === true} -