mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
Merge 14f6d0cc1f
into aef0ad2d10
This commit is contained in:
commit
8b675f9cb4
@ -0,0 +1,22 @@
|
||||
"""Add system prompt to folder
|
||||
|
||||
Revision ID: e7f8a9b2c5d1
|
||||
Revises: 9f0c9cd09105
|
||||
Create Date: 2025-06-21 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e7f8a9b2c5d1'
|
||||
down_revision: Union[str, None] = '9f0c9cd09105'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('folder', sa.Column('system_prompt', sa.Text(), nullable=True))
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('folder', 'system_prompt')
|
@ -66,6 +66,7 @@ class ChatModel(BaseModel):
|
||||
|
||||
class ChatForm(BaseModel):
|
||||
chat: dict
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChatImportForm(ChatForm):
|
||||
@ -118,6 +119,7 @@ class ChatTable:
|
||||
else "New Chat"
|
||||
),
|
||||
"chat": form_data.chat,
|
||||
"folder_id": form_data.folder_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ class Folder(Base):
|
||||
items = Column(JSON, nullable=True)
|
||||
meta = Column(JSON, nullable=True)
|
||||
is_expanded = Column(Boolean, default=False)
|
||||
system_prompt = Column(Text, nullable=True)
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
@ -42,6 +43,7 @@ class FolderModel(BaseModel):
|
||||
items: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
is_expanded: bool = False
|
||||
system_prompt: Optional[str] = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
@ -55,6 +57,7 @@ class FolderModel(BaseModel):
|
||||
|
||||
class FolderForm(BaseModel):
|
||||
name: str
|
||||
system_prompt: Optional[str] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
@ -70,6 +73,7 @@ class FolderTable:
|
||||
"user_id": user_id,
|
||||
"name": name,
|
||||
"parent_id": parent_id,
|
||||
"system_prompt": None,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
@ -236,6 +240,49 @@ class FolderTable:
|
||||
log.error(f"update_folder: {e}")
|
||||
return
|
||||
|
||||
def update_folder_details_by_id_and_user_id(
|
||||
self,
|
||||
id: str,
|
||||
user_id: str,
|
||||
name: str,
|
||||
system_prompt: Optional[str],
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
if not folder:
|
||||
return None
|
||||
|
||||
# Check if a folder with the new name already exists at the same parent level
|
||||
# Exclude the current folder being renamed from the check
|
||||
if name != folder.name:
|
||||
existing_folder = (
|
||||
db.query(Folder)
|
||||
.filter(
|
||||
Folder.name == name,
|
||||
Folder.parent_id == folder.parent_id,
|
||||
Folder.user_id == user_id,
|
||||
Folder.id != id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing_folder:
|
||||
log.warning(
|
||||
f"Folder with name '{name}' already exists for user_id '{user_id}' at parent_id '{folder.parent_id}'"
|
||||
)
|
||||
return None
|
||||
|
||||
folder.name = name
|
||||
folder.system_prompt = system_prompt
|
||||
folder.updated_at = int(time.time())
|
||||
db.commit()
|
||||
db.refresh(folder)
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"Error updating folder details: {e}")
|
||||
db.rollback()
|
||||
return None
|
||||
|
||||
def delete_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str, delete_chats=True
|
||||
) -> bool:
|
||||
|
@ -105,33 +105,33 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Update Folder Name By Id
|
||||
# Update Folder Details By Id
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/update")
|
||||
async def update_folder_name_by_id(
|
||||
async def update_folder_by_id(
|
||||
id: str, form_data: FolderForm, user=Depends(get_verified_user)
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
if folder:
|
||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
folder.parent_id, user.id, form_data.name
|
||||
try:
|
||||
updated_folder = Folders.update_folder_details_by_id_and_user_id(
|
||||
id=id,
|
||||
user_id=user.id,
|
||||
name=form_data.name,
|
||||
system_prompt=form_data.system_prompt,
|
||||
)
|
||||
if existing_folder:
|
||||
if updated_folder:
|
||||
return updated_folder
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
|
||||
detail=ERROR_MESSAGES.DEFAULT(
|
||||
"Could not update folder. It might be due to a name conflict or invalid data."
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
folder = Folders.update_folder_name_by_id_and_user_id(
|
||||
id, user.id, form_data.name
|
||||
)
|
||||
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error updating folder: {id}")
|
||||
|
@ -15,6 +15,8 @@ from starlette.responses import Response, StreamingResponse, JSONResponse
|
||||
|
||||
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.models.chats import Chats
|
||||
from open_webui.models.folders import Folders
|
||||
|
||||
from open_webui.socket.main import (
|
||||
sio,
|
||||
@ -165,6 +167,29 @@ async def generate_chat_completion(
|
||||
bypass_filter: bool = False,
|
||||
):
|
||||
log.debug(f"generate_chat_completion: {form_data}")
|
||||
metadata = form_data.get("metadata", {})
|
||||
if hasattr(request.state, "metadata"):
|
||||
metadata.update(request.state.metadata)
|
||||
form_data["metadata"] = metadata
|
||||
|
||||
chat_id = metadata.get("chat_id")
|
||||
if chat_id and user:
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
if chat and chat.folder_id:
|
||||
folder = Folders.get_folder_by_id_and_user_id(chat.folder_id, user.id)
|
||||
if folder and folder.system_prompt:
|
||||
folder_system_message = {
|
||||
"role": "system",
|
||||
"content": folder.system_prompt,
|
||||
}
|
||||
# Ensure messages list exists
|
||||
if "messages" not in form_data:
|
||||
form_data["messages"] = []
|
||||
|
||||
form_data["messages"] = [folder_system_message] + form_data["messages"]
|
||||
log.debug(
|
||||
f"Prepended system prompt from folder {folder.id} to chat {chat_id}"
|
||||
)
|
||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||
bypass_filter = True
|
||||
|
||||
|
@ -4,6 +4,8 @@ import { getTimeRange } from '$lib/utils';
|
||||
export const createNewChat = async (token: string, chat: object) => {
|
||||
let error = null;
|
||||
|
||||
const { folder_id, ...chatData } = chat as any;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -12,7 +14,8 @@ export const createNewChat = async (token: string, chat: object) => {
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat: chat
|
||||
chat: chatData,
|
||||
...(folder_id && { folder_id: folder_id })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
@ -92,7 +92,11 @@ export const getFolderById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateFolderNameById = async (token: string, id: string, name: string) => {
|
||||
export const updateFolderById = async (
|
||||
token: string,
|
||||
id: string,
|
||||
folderData: { name: string; system_prompt?: string }
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, {
|
||||
@ -102,9 +106,7 @@ export const updateFolderNameById = async (token: string, id: string, name: stri
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name
|
||||
})
|
||||
body: JSON.stringify(folderData)
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
|
@ -36,7 +36,11 @@
|
||||
chatTitle,
|
||||
showArtifacts,
|
||||
tools,
|
||||
toolServers
|
||||
toolServers,
|
||||
pendingFolderId,
|
||||
pendingFolderName,
|
||||
folders,
|
||||
refreshSidebar
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@ -65,6 +69,7 @@
|
||||
getTagsById,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { getFolders } from '$lib/apis/folders';
|
||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
@ -308,11 +313,9 @@
|
||||
}
|
||||
} else if (type === 'chat:title') {
|
||||
chatTitle.set(data);
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await $refreshSidebar();
|
||||
} else if (type === 'chat:tags') {
|
||||
chat = await getChatById(localStorage.token, $chatId);
|
||||
allTags.set(await getAllTags(localStorage.token));
|
||||
await $refreshSidebar();
|
||||
} else if (type === 'source' || type === 'citation') {
|
||||
if (data?.type === 'code_execution') {
|
||||
// Code execution; update existing code execution by ID, or add new one.
|
||||
@ -708,6 +711,7 @@
|
||||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
|
||||
const availableModels = $models
|
||||
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
||||
.map((m) => m.id);
|
||||
@ -1958,14 +1962,17 @@
|
||||
history: history,
|
||||
messages: createMessagesList(history, history.currentId),
|
||||
tags: [],
|
||||
folder_id: $pendingFolderId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
_chatId = chat.id;
|
||||
await chatId.set(_chatId);
|
||||
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
currentChatPage.set(1);
|
||||
// Clear the pending folder after creating the chat
|
||||
$refreshSidebar();
|
||||
pendingFolderId.set(null);
|
||||
pendingFolderName.set(null);
|
||||
|
||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
} else {
|
||||
|
@ -2,7 +2,9 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
|
||||
import { config, user, models as _models, temporaryChatEnabled, pendingFolderId, pendingFolderName } from '$lib/stores';
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { blur, fade } from 'svelte/transition';
|
||||
@ -80,6 +82,27 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $pendingFolderName}
|
||||
<div class="mt-4 mb-4 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-xl">
|
||||
<div class="flex items-center gap-2 text-blue-700 dark:text-blue-300">
|
||||
<FolderOpen className="size-4" />
|
||||
<span class="text-sm font-medium">
|
||||
{$i18n.t('Next chat will be created in folder:')}
|
||||
<strong>{$pendingFolderName}</strong>
|
||||
</span>
|
||||
<button
|
||||
class="ml-auto text-blue-500 hover:text-blue-700"
|
||||
on:click={() => {
|
||||
pendingFolderId.set(null);
|
||||
pendingFolderName.set(null);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-medium text-left flex items-center gap-4 font-primary"
|
||||
>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
|
||||
import { config, user, models as _models, temporaryChatEnabled, pendingFolderId, pendingFolderName } from '$lib/stores';
|
||||
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||
import MessageInput from './MessageInput.svelte';
|
||||
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let transparentBackground = false;
|
||||
@ -103,6 +106,27 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $pendingFolderName}
|
||||
<div class="mb-2 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg mx-auto max-w-xl">
|
||||
<div class="flex items-center gap-2 text-blue-700 dark:text-blue-300">
|
||||
<FolderOpen className="size-4" />
|
||||
<span class="text-sm font-medium">
|
||||
{$i18n.t('Next chat will be created in folder:')}
|
||||
<strong>{$pendingFolderName}</strong>
|
||||
</span>
|
||||
<button
|
||||
class="ml-auto text-blue-500 hover:text-blue-700"
|
||||
on:click={() => {
|
||||
pendingFolderId.set(null);
|
||||
pendingFolderName.set(null);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
|
||||
>
|
||||
|
@ -22,7 +22,11 @@
|
||||
socket,
|
||||
config,
|
||||
isApp,
|
||||
models
|
||||
models,
|
||||
pendingFolderId,
|
||||
pendingFolderName,
|
||||
folders,
|
||||
refreshSidebar
|
||||
} from '$lib/stores';
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
|
||||
@ -77,7 +81,6 @@
|
||||
let chatListLoading = false;
|
||||
let allChatsLoaded = false;
|
||||
|
||||
let folders = {};
|
||||
let newFolderId = null;
|
||||
|
||||
const initFolders = async () => {
|
||||
@ -86,15 +89,15 @@
|
||||
return [];
|
||||
});
|
||||
|
||||
folders = {};
|
||||
let newFolders = {};
|
||||
|
||||
// First pass: Initialize all folder entries
|
||||
for (const folder of folderList) {
|
||||
// Ensure folder is added to folders with its data
|
||||
folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
|
||||
newFolders[folder.id] = { ...(newFolders[folder.id] || {}), ...folder };
|
||||
|
||||
if (newFolderId && folder.id === newFolderId) {
|
||||
folders[folder.id].new = true;
|
||||
newFolders[folder.id].new = true;
|
||||
newFolderId = null;
|
||||
}
|
||||
}
|
||||
@ -103,30 +106,31 @@
|
||||
for (const folder of folderList) {
|
||||
if (folder.parent_id) {
|
||||
// Ensure the parent folder is initialized if it doesn't exist
|
||||
if (!folders[folder.parent_id]) {
|
||||
folders[folder.parent_id] = {}; // Create a placeholder if not already present
|
||||
if (!newFolders[folder.parent_id]) {
|
||||
newFolders[folder.parent_id] = {}; // Create a placeholder if not already present
|
||||
}
|
||||
|
||||
// Initialize childrenIds array if it doesn't exist and add the current folder id
|
||||
folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
|
||||
? [...folders[folder.parent_id].childrenIds, folder.id]
|
||||
newFolders[folder.parent_id].childrenIds = newFolders[folder.parent_id].childrenIds
|
||||
? [...newFolders[folder.parent_id].childrenIds, folder.id]
|
||||
: [folder.id];
|
||||
|
||||
// Sort the children by updated_at field
|
||||
folders[folder.parent_id].childrenIds.sort((a, b) => {
|
||||
return folders[b].updated_at - folders[a].updated_at;
|
||||
newFolders[folder.parent_id].childrenIds.sort((a, b) => {
|
||||
return newFolders[b].updated_at - newFolders[a].updated_at;
|
||||
});
|
||||
}
|
||||
}
|
||||
folders.set(newFolders);
|
||||
};
|
||||
|
||||
const createFolder = async (name = 'Untitled') => {
|
||||
const createFolder = async (name = '📁 Untitled Folder') => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
|
||||
const rootFolders = Object.values($folders).filter((folder) => folder.parent_id === null);
|
||||
if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
|
||||
// If a folder with the same name already exists, append a number to the name
|
||||
let i = 1;
|
||||
@ -138,18 +142,17 @@
|
||||
|
||||
name = `${name} ${i}`;
|
||||
}
|
||||
|
||||
// Add a dummy folder to the list to show the user that the folder is being created
|
||||
const tempId = uuidv4();
|
||||
folders = {
|
||||
...folders,
|
||||
folders.set({
|
||||
...$folders,
|
||||
tempId: {
|
||||
id: tempId,
|
||||
name: name,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const res = await createNewFolder(localStorage.token, name).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
@ -162,6 +165,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
const createChatInFolderHandler = async (event) => {
|
||||
const { folderId } = event.detail;
|
||||
|
||||
pendingFolderId.set(folderId);
|
||||
pendingFolderName.set($folders[folderId].name);
|
||||
|
||||
await goto('/');
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
const initChannels = async () => {
|
||||
await channels.set(await getChannels(localStorage.token));
|
||||
};
|
||||
@ -181,6 +197,8 @@
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
refreshSidebar.set(initChatList);
|
||||
|
||||
const loadMoreChats = async () => {
|
||||
chatListLoading = true;
|
||||
|
||||
@ -751,7 +769,7 @@
|
||||
initChatList();
|
||||
}
|
||||
} else if (type === 'folder') {
|
||||
if (folders[id].parent_id === null) {
|
||||
if ($folders[id].parent_id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -763,7 +781,7 @@
|
||||
);
|
||||
|
||||
if (res) {
|
||||
await initFolders();
|
||||
initChatList();
|
||||
}
|
||||
}
|
||||
}}
|
||||
@ -844,9 +862,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if folders}
|
||||
{#if $folders}
|
||||
<Folders
|
||||
{folders}
|
||||
folders={$folders}
|
||||
on:import={(e) => {
|
||||
const { folderId, items } = e.detail;
|
||||
importChatHandler(items, false, folderId);
|
||||
@ -857,6 +875,7 @@
|
||||
on:change={async () => {
|
||||
initChatList();
|
||||
}}
|
||||
on:createChatInFolder={createChatInFolderHandler}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -1,11 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||
import EditFolderModal from './Folders/EditFolderModal.svelte';
|
||||
import { updateFolderById } from '$lib/apis/folders';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let folders = {};
|
||||
|
||||
let folderList = [];
|
||||
|
||||
// Get the list of folders that have no parent, sorted by name alphabetically
|
||||
$: folderList = Object.keys(folders)
|
||||
.filter((key) => folders[key].parent_id === null)
|
||||
@ -15,8 +21,63 @@
|
||||
sensitivity: 'base'
|
||||
})
|
||||
);
|
||||
|
||||
let showEditFolderModal = false;
|
||||
let currentEditingFolder = null;
|
||||
|
||||
const handleEditFolderEvent = (event) => {
|
||||
const folderToEdit = event.detail;
|
||||
if (folderToEdit && folderToEdit.id) {
|
||||
currentEditingFolder = { ...folderToEdit };
|
||||
showEditFolderModal = true;
|
||||
} else {
|
||||
console.error('EditFolder event did not provide valid folder data:', event.detail);
|
||||
toast.error($i18n.t('Failed to open edit dialog: Invalid folder data.'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFolderEdit = async (event) => {
|
||||
const { id, name, system_prompt } = event.detail;
|
||||
|
||||
try {
|
||||
const updatedFolder = await updateFolderById(localStorage.token, id, {
|
||||
name,
|
||||
system_prompt
|
||||
});
|
||||
|
||||
if (updatedFolder && updatedFolder.id) {
|
||||
toast.success($i18n.t('Folder updated successfully'));
|
||||
showEditFolderModal = false;
|
||||
currentEditingFolder = null;
|
||||
dispatch('update', updatedFolder);
|
||||
} else {
|
||||
const errorDetail = updatedFolder?.detail || $i18n.t('Unknown error');
|
||||
toast.error(`${$i18n.t('Failed to update folder')}: ${errorDetail}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update folder:', error);
|
||||
const errorDetail = error?.detail || error?.message || $i18n.t('Unknown error');
|
||||
toast.error(`${$i18n.t('Failed to update folder')}: ${errorDetail}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateChatInFolder = (event) => {
|
||||
dispatch('createChatInFolder', event.detail);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if showEditFolderModal && currentEditingFolder}
|
||||
<EditFolderModal
|
||||
bind:show={showEditFolderModal}
|
||||
folder={currentEditingFolder}
|
||||
on:saveFolder={handleSaveFolderEdit}
|
||||
on:close={() => {
|
||||
showEditFolderModal = false;
|
||||
currentEditingFolder = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each folderList as folderId (folderId)}
|
||||
<RecursiveFolder
|
||||
className=""
|
||||
@ -31,5 +92,7 @@
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
on:editFolder={handleEditFolderEvent}
|
||||
on:createChatInFolder={handleCreateChatInFolder}
|
||||
/>
|
||||
{/each}
|
||||
|
139
src/lib/components/layout/Sidebar/Folders/EditFolderModal.svelte
Normal file
139
src/lib/components/layout/Sidebar/Folders/EditFolderModal.svelte
Normal file
@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import { user } from '$lib/stores';
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
export let show = false;
|
||||
export let folder = { id: '', name: '', system_prompt: '' };
|
||||
let currentName = '';
|
||||
let currentSystemPrompt = '';
|
||||
let loading = false;
|
||||
let initialized = false;
|
||||
// Initialize values once when modal opens
|
||||
$: if (show && folder && !initialized) {
|
||||
currentName = folder.name || '';
|
||||
currentSystemPrompt = folder.system_prompt || '';
|
||||
initialized = true;
|
||||
}
|
||||
// Reset when modal closes
|
||||
$: if (!show) {
|
||||
initialized = false;
|
||||
loading = false;
|
||||
}
|
||||
// Auto-focus name input when modal opens
|
||||
$: if (show && initialized) {
|
||||
setTimeout(() => {
|
||||
document.getElementById('folderName')?.focus();
|
||||
}, 150);
|
||||
}
|
||||
const handleSubmit = () => {
|
||||
if (!currentName.trim()) return;
|
||||
loading = true;
|
||||
dispatch('saveFolder', {
|
||||
id: folder.id,
|
||||
name: currentName.trim(),
|
||||
system_prompt: currentSystemPrompt.trim() || null
|
||||
});
|
||||
};
|
||||
const handleClose = () => {
|
||||
loading = false;
|
||||
initialized = false;
|
||||
dispatch('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show on:close={handleClose} size="md">
|
||||
<div>
|
||||
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-1.5">
|
||||
<div class="text-lg font-medium self-center font-primary">
|
||||
{$i18n.t('Edit Folder')}
|
||||
</div>
|
||||
<button
|
||||
class="self-center p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||
on:click={handleClose}
|
||||
aria-label={$i18n.t('Close')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
class="flex flex-col w-full px-6 pb-6 md:space-y-5 dark:text-gray-200"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<div class="px-1 space-y-4">
|
||||
<div>
|
||||
<label for="folderName" class="block mb-1 text-xs text-gray-500">
|
||||
{$i18n.t('Folder Name')}
|
||||
</label>
|
||||
<input
|
||||
id="folderName"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-400 dark:placeholder:text-gray-600 outline-none border border-gray-300 dark:border-gray-700 rounded-lg p-2 focus:ring-1 focus:ring-blue-500"
|
||||
type="text"
|
||||
bind:value={currentName}
|
||||
placeholder={$i18n.t('Enter folder name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
|
||||
<div>
|
||||
<label for="systemPrompt" class="block mb-1 text-xs text-gray-500">
|
||||
{$i18n.t('System Prompt (Optional)')}
|
||||
</label>
|
||||
<Textarea
|
||||
id="systemPrompt"
|
||||
bind:value={currentSystemPrompt}
|
||||
placeholder={$i18n.t('Enter a system prompt for all chats in this folder...')}
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-400 dark:placeholder:text-gray-600 outline-none border border-gray-300 dark:border-gray-700 rounded-lg p-2 focus:ring-1 focus:ring-blue-500 min-h-[120px] max-h-[600px] overflow-y-auto resize-none"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end items-center pt-4 text-sm font-medium gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-gray-700 dark:hover:bg-gray-600 bg-gray-100 hover:bg-gray-200 text-black dark:text-white transition rounded-full"
|
||||
on:click={handleClose}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-200 transition rounded-full flex items-center"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="mr-2 self-center">
|
||||
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="32"
|
||||
stroke-dashoffset="32"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
@ -38,11 +38,11 @@
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('rename');
|
||||
dispatch('editFolder');
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
<div class="flex items-center">{$i18n.t('Edit Folder')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@ -12,13 +11,18 @@
|
||||
import ChevronRight from '../../icons/ChevronRight.svelte';
|
||||
import Collapsible from '../../common/Collapsible.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
import Plus from '../../icons/Plus.svelte';
|
||||
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
import { pendingFolderId, pendingFolderName, mobile, showSidebar } from '$lib/stores';
|
||||
|
||||
import {
|
||||
deleteFolderById,
|
||||
updateFolderIsExpandedById,
|
||||
updateFolderNameById,
|
||||
updateFolderParentIdById
|
||||
} from '$lib/apis/folders';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@ -28,6 +32,7 @@
|
||||
importChat,
|
||||
updateChatFolderIdById
|
||||
} from '$lib/apis/chats';
|
||||
|
||||
import ChatItem from './ChatItem.svelte';
|
||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
@ -43,13 +48,9 @@
|
||||
|
||||
let folderElement;
|
||||
|
||||
let edit = false;
|
||||
|
||||
let draggedOver = false;
|
||||
let dragged = false;
|
||||
|
||||
let name = '';
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -172,6 +173,7 @@
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
|
||||
// Set the data to be transferred
|
||||
@ -196,7 +198,6 @@
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
folderElement.style.opacity = '1'; // Reset visual cue after drag
|
||||
dragged = false;
|
||||
};
|
||||
@ -216,17 +217,11 @@
|
||||
folderElement.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
|
||||
if (folders[folderId]?.new) {
|
||||
delete folders[folderId].new;
|
||||
|
||||
await tick();
|
||||
editHandler();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
|
||||
@ -250,36 +245,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const nameUpdateHandler = async () => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === folders[folderId].name) {
|
||||
edit = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentName = folders[folderId].name;
|
||||
|
||||
name = name.trim();
|
||||
folders[folderId].name = name;
|
||||
|
||||
const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
|
||||
folders[folderId].name = currentName;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
folders[folderId].name = name;
|
||||
toast.success($i18n.t('Folder name updated successfully'));
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const isExpandedUpdateHandler = async () => {
|
||||
const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
|
||||
(error) => {
|
||||
@ -300,21 +265,6 @@
|
||||
|
||||
$: isExpandedUpdateDebounceHandler(open);
|
||||
|
||||
const editHandler = async () => {
|
||||
console.log('Edit');
|
||||
await tick();
|
||||
name = folders[folderId].name;
|
||||
|
||||
edit = true;
|
||||
await tick();
|
||||
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const exportHandler = async () => {
|
||||
const chats = await getChatsByFolderId(localStorage.token, folderId).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
@ -383,9 +333,6 @@
|
||||
<button
|
||||
id="folder-{folderId}-button"
|
||||
class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:dblclick={() => {
|
||||
editHandler();
|
||||
}}
|
||||
>
|
||||
<div class="text-gray-300 dark:text-gray-600">
|
||||
{#if open}
|
||||
@ -396,51 +343,28 @@
|
||||
</div>
|
||||
|
||||
<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
|
||||
{#if edit}
|
||||
<input
|
||||
id="folder-{folderId}-input"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
on:focus={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
on:blur={() => {
|
||||
nameUpdateHandler();
|
||||
edit = false;
|
||||
}}
|
||||
on:click={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:mousedown={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
nameUpdateHandler();
|
||||
edit = false;
|
||||
}
|
||||
}}
|
||||
class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-hidden"
|
||||
/>
|
||||
{:else}
|
||||
{folders[folderId].name}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center gap-1 dark:text-gray-300">
|
||||
<!-- Plus Button -->
|
||||
<button
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||
class="p-0.5 dark:hover:bg-gray-850 hover:bg-gray-200 rounded-lg touch-auto" on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch('createChatInFolder', { folderId });
|
||||
}}
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
title={$i18n.t('Create new chat in folder')}
|
||||
>
|
||||
<Plus className="size-3.5" strokeWidth="2.5" />
|
||||
</button>
|
||||
|
||||
<!-- Three-dot Menu -->
|
||||
<FolderMenu
|
||||
on:rename={() => {
|
||||
// Requires a timeout to prevent the click event from closing the dropdown
|
||||
setTimeout(() => {
|
||||
editHandler();
|
||||
}, 200);
|
||||
on:editFolder={() => {
|
||||
dispatch('editFolder', folders[folderId]);
|
||||
}}
|
||||
on:delete={() => {
|
||||
showDeleteConfirm = true;
|
||||
@ -449,11 +373,11 @@
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
||||
<button class="p-0.5 dark:hover:bg-gray-850 hover:bg-gray-200 rounded-lg touch-auto" on:click={(e) => {}}>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
</FolderMenu>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -486,6 +410,12 @@
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
on:editFolder={(e) => {
|
||||
dispatch('editFolder', e.detail);
|
||||
}}
|
||||
on:createChatInFolder={(e) => {
|
||||
dispatch('createChatInFolder', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -46,6 +46,11 @@ export const TTSWorker = writable(null);
|
||||
export const chatId = writable('');
|
||||
export const chatTitle = writable('');
|
||||
|
||||
export const refreshSidebar = writable(() => {});
|
||||
export const pendingFolderId = writable(null);
|
||||
export const pendingFolderName = writable(null);
|
||||
export const folders = writable({});
|
||||
|
||||
export const channels = writable([]);
|
||||
export const chats = writable(null);
|
||||
export const pinnedChats = writable([]);
|
||||
|
Loading…
Reference in New Issue
Block a user