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):
|
class ChatForm(BaseModel):
|
||||||
chat: dict
|
chat: dict
|
||||||
|
folder_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ChatImportForm(ChatForm):
|
class ChatImportForm(ChatForm):
|
||||||
@ -118,6 +119,7 @@ class ChatTable:
|
|||||||
else "New Chat"
|
else "New Chat"
|
||||||
),
|
),
|
||||||
"chat": form_data.chat,
|
"chat": form_data.chat,
|
||||||
|
"folder_id": form_data.folder_id,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"updated_at": int(time.time()),
|
"updated_at": int(time.time()),
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ class Folder(Base):
|
|||||||
items = Column(JSON, nullable=True)
|
items = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
is_expanded = Column(Boolean, default=False)
|
is_expanded = Column(Boolean, default=False)
|
||||||
|
system_prompt = Column(Text, nullable=True)
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ class FolderModel(BaseModel):
|
|||||||
items: Optional[dict] = None
|
items: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
is_expanded: bool = False
|
is_expanded: bool = False
|
||||||
|
system_prompt: Optional[str] = None
|
||||||
created_at: int
|
created_at: int
|
||||||
updated_at: int
|
updated_at: int
|
||||||
|
|
||||||
@ -55,6 +57,7 @@ class FolderModel(BaseModel):
|
|||||||
|
|
||||||
class FolderForm(BaseModel):
|
class FolderForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
system_prompt: Optional[str] = None
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +73,7 @@ class FolderTable:
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"parent_id": parent_id,
|
"parent_id": parent_id,
|
||||||
|
"system_prompt": None,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"updated_at": int(time.time()),
|
"updated_at": int(time.time()),
|
||||||
}
|
}
|
||||||
@ -236,6 +240,49 @@ class FolderTable:
|
|||||||
log.error(f"update_folder: {e}")
|
log.error(f"update_folder: {e}")
|
||||||
return
|
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(
|
def delete_folder_by_id_and_user_id(
|
||||||
self, id: str, user_id: str, delete_chats=True
|
self, id: str, user_id: str, delete_chats=True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@ -105,33 +105,33 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
|||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Update Folder Name By Id
|
# Update Folder Details By Id
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{id}/update")
|
@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)
|
id: str, form_data: FolderForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||||
if folder:
|
if folder:
|
||||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
|
||||||
folder.parent_id, user.id, form_data.name
|
|
||||||
)
|
|
||||||
if existing_folder:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
folder = Folders.update_folder_name_by_id_and_user_id(
|
updated_folder = Folders.update_folder_details_by_id_and_user_id(
|
||||||
id, user.id, form_data.name
|
id=id,
|
||||||
|
user_id=user.id,
|
||||||
|
name=form_data.name,
|
||||||
|
system_prompt=form_data.system_prompt,
|
||||||
)
|
)
|
||||||
|
if updated_folder:
|
||||||
return folder
|
return updated_folder
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(
|
||||||
|
"Could not update folder. It might be due to a name conflict or invalid data."
|
||||||
|
),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
log.error(f"Error updating folder: {id}")
|
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.users import UserModel
|
||||||
|
from open_webui.models.chats import Chats
|
||||||
|
from open_webui.models.folders import Folders
|
||||||
|
|
||||||
from open_webui.socket.main import (
|
from open_webui.socket.main import (
|
||||||
sio,
|
sio,
|
||||||
@ -165,6 +167,29 @@ async def generate_chat_completion(
|
|||||||
bypass_filter: bool = False,
|
bypass_filter: bool = False,
|
||||||
):
|
):
|
||||||
log.debug(f"generate_chat_completion: {form_data}")
|
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:
|
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||||
bypass_filter = True
|
bypass_filter = True
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import { getTimeRange } from '$lib/utils';
|
|||||||
export const createNewChat = async (token: string, chat: object) => {
|
export const createNewChat = async (token: string, chat: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
const { folder_id, ...chatData } = chat as any;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -12,7 +14,8 @@ export const createNewChat = async (token: string, chat: object) => {
|
|||||||
authorization: `Bearer ${token}`
|
authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chat: chat
|
chat: chatData,
|
||||||
|
...(folder_id && { folder_id: folder_id })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
@ -92,7 +92,11 @@ export const getFolderById = async (token: string, id: string) => {
|
|||||||
return res;
|
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;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, {
|
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',
|
'Content-Type': 'application/json',
|
||||||
authorization: `Bearer ${token}`
|
authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(folderData)
|
||||||
name: name
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) throw await res.json();
|
if (!res.ok) throw await res.json();
|
||||||
|
@ -36,7 +36,11 @@
|
|||||||
chatTitle,
|
chatTitle,
|
||||||
showArtifacts,
|
showArtifacts,
|
||||||
tools,
|
tools,
|
||||||
toolServers
|
toolServers,
|
||||||
|
pendingFolderId,
|
||||||
|
pendingFolderName,
|
||||||
|
folders,
|
||||||
|
refreshSidebar
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
@ -65,6 +69,7 @@
|
|||||||
getTagsById,
|
getTagsById,
|
||||||
updateChatById
|
updateChatById
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
|
import { getFolders } from '$lib/apis/folders';
|
||||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||||
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
|
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
|
||||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||||
@ -308,11 +313,9 @@
|
|||||||
}
|
}
|
||||||
} else if (type === 'chat:title') {
|
} else if (type === 'chat:title') {
|
||||||
chatTitle.set(data);
|
chatTitle.set(data);
|
||||||
currentChatPage.set(1);
|
await $refreshSidebar();
|
||||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
|
||||||
} else if (type === 'chat:tags') {
|
} else if (type === 'chat:tags') {
|
||||||
chat = await getChatById(localStorage.token, $chatId);
|
await $refreshSidebar();
|
||||||
allTags.set(await getAllTags(localStorage.token));
|
|
||||||
} else if (type === 'source' || type === 'citation') {
|
} else if (type === 'source' || type === 'citation') {
|
||||||
if (data?.type === 'code_execution') {
|
if (data?.type === 'code_execution') {
|
||||||
// Code execution; update existing code execution by ID, or add new one.
|
// Code execution; update existing code execution by ID, or add new one.
|
||||||
@ -708,7 +711,8 @@
|
|||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
const initNewChat = async () => {
|
const initNewChat = async () => {
|
||||||
const availableModels = $models
|
|
||||||
|
const availableModels = $models
|
||||||
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
||||||
.map((m) => m.id);
|
.map((m) => m.id);
|
||||||
|
|
||||||
@ -1958,14 +1962,17 @@
|
|||||||
history: history,
|
history: history,
|
||||||
messages: createMessagesList(history, history.currentId),
|
messages: createMessagesList(history, history.currentId),
|
||||||
tags: [],
|
tags: [],
|
||||||
|
folder_id: $pendingFolderId,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
_chatId = chat.id;
|
_chatId = chat.id;
|
||||||
await chatId.set(_chatId);
|
await chatId.set(_chatId);
|
||||||
|
|
||||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
// Clear the pending folder after creating the chat
|
||||||
currentChatPage.set(1);
|
$refreshSidebar();
|
||||||
|
pendingFolderId.set(null);
|
||||||
|
pendingFolderName.set(null);
|
||||||
|
|
||||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { marked } from 'marked';
|
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 { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
import { blur, fade } from 'svelte/transition';
|
import { blur, fade } from 'svelte/transition';
|
||||||
@ -80,6 +82,27 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/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
|
<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"
|
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();
|
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 { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
@ -16,6 +16,9 @@
|
|||||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||||
import MessageInput from './MessageInput.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');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let transparentBackground = false;
|
export let transparentBackground = false;
|
||||||
@ -103,6 +106,27 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/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
|
<div
|
||||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
|
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,
|
socket,
|
||||||
config,
|
config,
|
||||||
isApp,
|
isApp,
|
||||||
models
|
models,
|
||||||
|
pendingFolderId,
|
||||||
|
pendingFolderName,
|
||||||
|
folders,
|
||||||
|
refreshSidebar
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
|
|
||||||
@ -77,7 +81,6 @@
|
|||||||
let chatListLoading = false;
|
let chatListLoading = false;
|
||||||
let allChatsLoaded = false;
|
let allChatsLoaded = false;
|
||||||
|
|
||||||
let folders = {};
|
|
||||||
let newFolderId = null;
|
let newFolderId = null;
|
||||||
|
|
||||||
const initFolders = async () => {
|
const initFolders = async () => {
|
||||||
@ -86,15 +89,15 @@
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
folders = {};
|
let newFolders = {};
|
||||||
|
|
||||||
// First pass: Initialize all folder entries
|
// First pass: Initialize all folder entries
|
||||||
for (const folder of folderList) {
|
for (const folder of folderList) {
|
||||||
// Ensure folder is added to folders with its data
|
// 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) {
|
if (newFolderId && folder.id === newFolderId) {
|
||||||
folders[folder.id].new = true;
|
newFolders[folder.id].new = true;
|
||||||
newFolderId = null;
|
newFolderId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,30 +106,31 @@
|
|||||||
for (const folder of folderList) {
|
for (const folder of folderList) {
|
||||||
if (folder.parent_id) {
|
if (folder.parent_id) {
|
||||||
// Ensure the parent folder is initialized if it doesn't exist
|
// Ensure the parent folder is initialized if it doesn't exist
|
||||||
if (!folders[folder.parent_id]) {
|
if (!newFolders[folder.parent_id]) {
|
||||||
folders[folder.parent_id] = {}; // Create a placeholder if not already present
|
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
|
// Initialize childrenIds array if it doesn't exist and add the current folder id
|
||||||
folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
|
newFolders[folder.parent_id].childrenIds = newFolders[folder.parent_id].childrenIds
|
||||||
? [...folders[folder.parent_id].childrenIds, folder.id]
|
? [...newFolders[folder.parent_id].childrenIds, folder.id]
|
||||||
: [folder.id];
|
: [folder.id];
|
||||||
|
|
||||||
// Sort the children by updated_at field
|
// Sort the children by updated_at field
|
||||||
folders[folder.parent_id].childrenIds.sort((a, b) => {
|
newFolders[folder.parent_id].childrenIds.sort((a, b) => {
|
||||||
return folders[b].updated_at - folders[a].updated_at;
|
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 === '') {
|
if (name === '') {
|
||||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||||
return;
|
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 (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
|
||||||
// If a folder with the same name already exists, append a number to the name
|
// If a folder with the same name already exists, append a number to the name
|
||||||
let i = 1;
|
let i = 1;
|
||||||
@ -138,18 +142,17 @@
|
|||||||
|
|
||||||
name = `${name} ${i}`;
|
name = `${name} ${i}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a dummy folder to the list to show the user that the folder is being created
|
// Add a dummy folder to the list to show the user that the folder is being created
|
||||||
const tempId = uuidv4();
|
const tempId = uuidv4();
|
||||||
folders = {
|
folders.set({
|
||||||
...folders,
|
...$folders,
|
||||||
tempId: {
|
tempId: {
|
||||||
id: tempId,
|
id: tempId,
|
||||||
name: name,
|
name: name,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now()
|
updated_at: Date.now()
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const res = await createNewFolder(localStorage.token, name).catch((error) => {
|
const res = await createNewFolder(localStorage.token, name).catch((error) => {
|
||||||
toast.error(`${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 () => {
|
const initChannels = async () => {
|
||||||
await channels.set(await getChannels(localStorage.token));
|
await channels.set(await getChannels(localStorage.token));
|
||||||
};
|
};
|
||||||
@ -181,6 +197,8 @@
|
|||||||
scrollPaginationEnabled.set(true);
|
scrollPaginationEnabled.set(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
refreshSidebar.set(initChatList);
|
||||||
|
|
||||||
const loadMoreChats = async () => {
|
const loadMoreChats = async () => {
|
||||||
chatListLoading = true;
|
chatListLoading = true;
|
||||||
|
|
||||||
@ -751,7 +769,7 @@
|
|||||||
initChatList();
|
initChatList();
|
||||||
}
|
}
|
||||||
} else if (type === 'folder') {
|
} else if (type === 'folder') {
|
||||||
if (folders[id].parent_id === null) {
|
if ($folders[id].parent_id === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -763,7 +781,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
await initFolders();
|
initChatList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -844,9 +862,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if folders}
|
{#if $folders}
|
||||||
<Folders
|
<Folders
|
||||||
{folders}
|
folders={$folders}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
const { folderId, items } = e.detail;
|
const { folderId, items } = e.detail;
|
||||||
importChatHandler(items, false, folderId);
|
importChatHandler(items, false, folderId);
|
||||||
@ -857,6 +875,7 @@
|
|||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
initChatList();
|
initChatList();
|
||||||
}}
|
}}
|
||||||
|
on:createChatInFolder={createChatInFolderHandler}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
<script lang="ts">
|
<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();
|
const dispatch = createEventDispatcher();
|
||||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let folders = {};
|
export let folders = {};
|
||||||
|
|
||||||
let folderList = [];
|
let folderList = [];
|
||||||
|
|
||||||
// Get the list of folders that have no parent, sorted by name alphabetically
|
// Get the list of folders that have no parent, sorted by name alphabetically
|
||||||
$: folderList = Object.keys(folders)
|
$: folderList = Object.keys(folders)
|
||||||
.filter((key) => folders[key].parent_id === null)
|
.filter((key) => folders[key].parent_id === null)
|
||||||
@ -15,8 +21,63 @@
|
|||||||
sensitivity: 'base'
|
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>
|
</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)}
|
{#each folderList as folderId (folderId)}
|
||||||
<RecursiveFolder
|
<RecursiveFolder
|
||||||
className=""
|
className=""
|
||||||
@ -31,5 +92,7 @@
|
|||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
dispatch('change', e.detail);
|
dispatch('change', e.detail);
|
||||||
}}
|
}}
|
||||||
|
on:editFolder={handleEditFolderEvent}
|
||||||
|
on:createChatInFolder={handleCreateChatInFolder}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/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
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
dispatch('rename');
|
dispatch('editFolder');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil strokeWidth="2" />
|
<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>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@ -12,13 +11,18 @@
|
|||||||
import ChevronRight from '../../icons/ChevronRight.svelte';
|
import ChevronRight from '../../icons/ChevronRight.svelte';
|
||||||
import Collapsible from '../../common/Collapsible.svelte';
|
import Collapsible from '../../common/Collapsible.svelte';
|
||||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||||
|
import Plus from '../../icons/Plus.svelte';
|
||||||
|
|
||||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.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 {
|
import {
|
||||||
deleteFolderById,
|
deleteFolderById,
|
||||||
updateFolderIsExpandedById,
|
updateFolderIsExpandedById,
|
||||||
updateFolderNameById,
|
|
||||||
updateFolderParentIdById
|
updateFolderParentIdById
|
||||||
} from '$lib/apis/folders';
|
} from '$lib/apis/folders';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@ -28,6 +32,7 @@
|
|||||||
importChat,
|
importChat,
|
||||||
updateChatFolderIdById
|
updateChatFolderIdById
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
|
|
||||||
import ChatItem from './ChatItem.svelte';
|
import ChatItem from './ChatItem.svelte';
|
||||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
@ -43,13 +48,9 @@
|
|||||||
|
|
||||||
let folderElement;
|
let folderElement;
|
||||||
|
|
||||||
let edit = false;
|
|
||||||
|
|
||||||
let draggedOver = false;
|
let draggedOver = false;
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
||||||
let name = '';
|
|
||||||
|
|
||||||
const onDragOver = (e) => {
|
const onDragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -172,6 +173,7 @@
|
|||||||
|
|
||||||
const onDragStart = (event) => {
|
const onDragStart = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||||
|
|
||||||
// Set the data to be transferred
|
// Set the data to be transferred
|
||||||
@ -196,7 +198,6 @@
|
|||||||
|
|
||||||
const onDragEnd = (event) => {
|
const onDragEnd = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
folderElement.style.opacity = '1'; // Reset visual cue after drag
|
folderElement.style.opacity = '1'; // Reset visual cue after drag
|
||||||
dragged = false;
|
dragged = false;
|
||||||
};
|
};
|
||||||
@ -216,17 +217,11 @@
|
|||||||
folderElement.addEventListener('dragend', onDragEnd);
|
folderElement.addEventListener('dragend', onDragEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folders[folderId]?.new) {
|
|
||||||
delete folders[folderId].new;
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
editHandler();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (folderElement) {
|
if (folderElement) {
|
||||||
folderElement.addEventListener('dragover', onDragOver);
|
folderElement.removeEventListener('dragover', onDragOver);
|
||||||
folderElement.removeEventListener('drop', onDrop);
|
folderElement.removeEventListener('drop', onDrop);
|
||||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
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 isExpandedUpdateHandler = async () => {
|
||||||
const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
|
const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -300,21 +265,6 @@
|
|||||||
|
|
||||||
$: isExpandedUpdateDebounceHandler(open);
|
$: 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 exportHandler = async () => {
|
||||||
const chats = await getChatsByFolderId(localStorage.token, folderId).catch((error) => {
|
const chats = await getChatsByFolderId(localStorage.token, folderId).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
@ -383,9 +333,6 @@
|
|||||||
<button
|
<button
|
||||||
id="folder-{folderId}-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"
|
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">
|
<div class="text-gray-300 dark:text-gray-600">
|
||||||
{#if open}
|
{#if open}
|
||||||
@ -396,64 +343,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
|
<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
|
||||||
{#if edit}
|
{folders[folderId].name}
|
||||||
<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>
|
||||||
|
|
||||||
<button
|
<div class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center gap-1 dark:text-gray-300">
|
||||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
<!-- Plus Button -->
|
||||||
on:pointerup={(e) => {
|
<button
|
||||||
|
class="p-0.5 dark:hover:bg-gray-850 hover:bg-gray-200 rounded-lg touch-auto" on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
dispatch('createChatInFolder', { folderId });
|
||||||
}}
|
}}
|
||||||
>
|
on:pointerup={(e) => {
|
||||||
<FolderMenu
|
e.stopPropagation();
|
||||||
on:rename={() => {
|
}}
|
||||||
// Requires a timeout to prevent the click event from closing the dropdown
|
title={$i18n.t('Create new chat in folder')}
|
||||||
setTimeout(() => {
|
>
|
||||||
editHandler();
|
<Plus className="size-3.5" strokeWidth="2.5" />
|
||||||
}, 200);
|
</button>
|
||||||
}}
|
|
||||||
on:delete={() => {
|
<!-- Three-dot Menu -->
|
||||||
showDeleteConfirm = true;
|
<FolderMenu
|
||||||
}}
|
on:editFolder={() => {
|
||||||
on:export={() => {
|
dispatch('editFolder', folders[folderId]);
|
||||||
exportHandler();
|
}}
|
||||||
}}
|
on:delete={() => {
|
||||||
>
|
showDeleteConfirm = true;
|
||||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
}}
|
||||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
on:export={() => {
|
||||||
</button>
|
exportHandler();
|
||||||
</FolderMenu>
|
}}
|
||||||
</button>
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -486,6 +410,12 @@
|
|||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
dispatch('change', e.detail);
|
dispatch('change', e.detail);
|
||||||
}}
|
}}
|
||||||
|
on:editFolder={(e) => {
|
||||||
|
dispatch('editFolder', e.detail);
|
||||||
|
}}
|
||||||
|
on:createChatInFolder={(e) => {
|
||||||
|
dispatch('createChatInFolder', e.detail);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -46,6 +46,11 @@ export const TTSWorker = writable(null);
|
|||||||
export const chatId = writable('');
|
export const chatId = writable('');
|
||||||
export const chatTitle = 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 channels = writable([]);
|
||||||
export const chats = writable(null);
|
export const chats = writable(null);
|
||||||
export const pinnedChats = writable([]);
|
export const pinnedChats = writable([]);
|
||||||
|
Loading…
Reference in New Issue
Block a user