This commit is contained in:
Timothy Jaeryang Baek committed 2026-06-15 23:53:52 +02:00
1 parent 45fcf272ef
commit 76854d1424
10 files changed
+146 -83

No files matched your search

+9
View File
@@ -443,6 +443,15 @@ async def get_shared_folder_chats(
chats = await Chats.get_all_chats_by_folder_id(id, db=db)
# Resolve owner names for display (avatar URLs are constructed client-side)
owner_cache: dict[str, str] = {}
for chat in chats:
uid = chat['user_id']
if uid not in owner_cache:
u = await Users.get_user_by_id(uid, db=db)
owner_cache[uid] = u.name if u else 'Unknown'
chat['owner_name'] = owner_cache[uid]
return {
'chats': [
{**chat, 'readonly': chat['user_id'] != user.id}
+1
View File
@@ -3030,6 +3030,7 @@
<FilesOverlay show={dragged} />
<Navbar
bind:this={navbarElement}
{readOnly}
chat={{
id: $chatId,
chat: {
+3 -1
View File
@@ -43,6 +43,7 @@
const i18n = getContext('i18n');
export let initNewChat: Function;
export let readOnly: boolean = false;
export let shareEnabled: boolean = false;
export let scrollTop = 0;
export let scrollToTop: (() => void) | null = null;
@@ -122,7 +123,7 @@
"
>
{#if showModelSelector}
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled && !readOnly} disabled={readOnly} />
{/if}
</div>
@@ -203,6 +204,7 @@
<Menu
{chat}
{shareEnabled}
{readOnly}
{scrollToTop}
shareHandler={() => {
showShareChatModal = !showShareChatModal;
+44 -38
View File
@@ -73,6 +73,9 @@
}
$: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
// True when viewing a shared folder the current user doesn't own
$: folderReadOnly = $selectedFolder != null && $selectedFolder.user_id !== $user?.id;
</script>
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
@@ -94,18 +97,19 @@
<div class="w-full flex flex-col justify-center items-center">
{#if $selectedFolder}
<FolderTitle
folder={$selectedFolder}
onUpdate={async (folder) => {
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
}}
onDelete={async () => {
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
folder={$selectedFolder}
readOnly={folderReadOnly}
onUpdate={async (folder) => {
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
}}
onDelete={async () => {
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
selectedFolder.set(null);
}}
/>
selectedFolder.set(null);
}}
/>
{:else}
<div class="flex flex-row justify-center gap-2.5 @sm:gap-3 w-fit px-5 max-w-xl">
<div class="flex shrink-0 justify-center">
@@ -210,33 +214,35 @@
{/if}
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
<MessageInput
bind:this={messageInput}
{history}
{selectedModels}
bind:files
bind:prompt
bind:autoScroll
bind:selectedToolIds
bind:selectedSkillIds
bind:selectedFilterIds
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
bind:webSearchEnabled
bind:atSelectedModel
bind:showCommands
bind:dragged
{pendingOAuthTools}
{toolServers}
{stopResponse}
{createMessagePair}
placeholder={$i18n.t('How can I help you today?')}
{onChange}
{onUpload}
on:submit={(e) => {
dispatch('submit', e.detail);
}}
/>
{#if !($selectedFolder && folderReadOnly)}
<MessageInput
bind:this={messageInput}
{history}
{selectedModels}
bind:files
bind:prompt
bind:autoScroll
bind:selectedToolIds
bind:selectedSkillIds
bind:selectedFilterIds
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
bind:webSearchEnabled
bind:atSelectedModel
bind:showCommands
bind:dragged
{pendingOAuthTools}
{toolServers}
{stopResponse}
{createMessagePair}
placeholder={$i18n.t('How can I help you today?')}
{onChange}
{onUpload}
on:submit={(e) => {
dispatch('submit', e.detail);
}}
/>
{/if}
</div>
</div>
</div>
@@ -9,6 +9,7 @@
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Loader from '$lib/components/common/Loader.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
dayjs.extend(localizedFormat);
@@ -54,6 +55,7 @@
let orderBy = 'updated_at';
let direction = 'desc'; // 'asc' or 'desc'
$: if (chats) {
init();
}
@@ -157,10 +159,20 @@
{chat?.title}
</div>
<div class="hidden sm:flex sm:basis-2/5 items-center justify-end">
<div class="hidden sm:flex sm:basis-2/5 items-center justify-end gap-2">
<div class=" text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat?.updated_at * 1000).calendar()}
</div>
{#if chat.user_id && chat.owner_name}
<Tooltip content={chat.owner_name}>
<img
src="/api/v1/users/{chat.user_id}/profile/image"
alt=""
class="size-5 rounded-full shrink-0 object-cover"
/>
</Tooltip>
{/if}
</div>
</a>
{/each}
@@ -31,6 +31,7 @@
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
export let folder = null;
export let readOnly: boolean = false;
export let onUpdate: Function = (folderId) => {};
export let onDelete: Function = (folderId) => {};
@@ -205,58 +206,72 @@
<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
<div class="text-center flex gap-3.5 items-center">
<EmojiPicker
onClose={() => {}}
selected={folder?.meta?.icon ?? null}
onSubmit={(name) => {
console.log(name);
updateIconHandler(name);
}}
>
<button
aria-label={$i18n.t('Change folder icon')}
class=" rounded-full bg-gray-50 dark:bg-gray-800 size-11 flex justify-center items-center"
{#if readOnly}
<div
class="rounded-full bg-gray-50 dark:bg-gray-800 size-11 flex justify-center items-center"
>
{#if folder?.meta?.icon}
<Emoji className="size-6" shortCode={folder.meta.icon} />
{:else}
<Folder className="size-4.5" strokeWidth="2" />
{/if}
</button>
</EmojiPicker>
</div>
{:else}
<EmojiPicker
onClose={() => {}}
selected={folder?.meta?.icon ?? null}
onSubmit={(name) => {
console.log(name);
updateIconHandler(name);
}}
>
<button
aria-label={$i18n.t('Change folder icon')}
class=" rounded-full bg-gray-50 dark:bg-gray-800 size-11 flex justify-center items-center"
>
{#if folder?.meta?.icon}
<Emoji className="size-6" shortCode={folder.meta.icon} />
{:else}
<Folder className="size-4.5" strokeWidth="2" />
{/if}
</button>
</EmojiPicker>
{/if}
<div class="text-3xl line-clamp-1">
{folder.name}
</div>
</div>
<div class="flex items-center translate-x-2.5">
<FolderMenu
align="end"
onEdit={() => {
showFolderModal = true;
}}
onShare={() => {
showShareModal = true;
}}
onDelete={() => {
showDeleteConfirm = true;
}}
onExport={() => {
exportHandler();
}}
onCreateSubFolder={() => {
showCreateSubFolderModal = true;
}}
>
<button
class="p-1.5 dark:hover:bg-gray-850 rounded-full touch-auto"
aria-label={$i18n.t('Folder options')}
on:click={(e) => {}}
{#if !readOnly}
<div class="flex items-center translate-x-2.5">
<FolderMenu
align="end"
onEdit={() => {
showFolderModal = true;
}}
onShare={() => {
showShareModal = true;
}}
onDelete={() => {
showDeleteConfirm = true;
}}
onExport={() => {
exportHandler();
}}
onCreateSubFolder={() => {
showCreateSubFolderModal = true;
}}
>
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
</button>
</FolderMenu>
</div>
<button
class="p-1.5 dark:hover:bg-gray-850 rounded-full touch-auto"
aria-label={$i18n.t('Folder options')}
on:click={(e) => {}}
>
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
</button>
</FolderMenu>
</div>
{/if}
</div>
{/if}
+3 -2
View File
@@ -39,6 +39,7 @@
const i18n = getContext('i18n');
export let shareEnabled: boolean = false;
export let readOnly: boolean = false;
export let shareHandler: Function;
export let moveChatHandler: Function;
@@ -364,7 +365,7 @@
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
{/if}
{#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
{#if !readOnly && !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
<button
draggable="false"
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-xl select-none w-full"
@@ -438,7 +439,7 @@
<div class="flex items-center">{$i18n.t('Copy')}</div>
</button>
{#if !$temporaryChatEnabled && chat?.id}
{#if !readOnly && !$temporaryChatEnabled && chat?.id}
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
{#if $folders.length > 0}
@@ -65,6 +65,9 @@
export let shiftKey = false;
export let readonly = false;
export let ownerName: string | null = null;
export let ownerUserId: string | null = null;
export let onDragEnd = () => {};
function formatTimeAgo(timestamp: number): string {
@@ -534,6 +537,15 @@
{/if}
<div class="flex self-center flex-1 w-full min-w-0">
{#if ownerUserId}
<Tooltip content={ownerName || 'Unknown'}>
<img
src="/api/v1/users/{ownerUserId}/profile/image"
alt=""
class="size-4 rounded-full shrink-0 object-cover mr-1.5"
/>
</Tooltip>
{/if}
{#if unread}
<div class="shrink-0 self-center pr-2.5 flex transition-opacity duration-300">
<div class="size-1.5 bg-sky-500 rounded-full" />
@@ -11,7 +11,7 @@
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
import { chatId, mobile, selectedFolder, showSidebar } from '$lib/stores';
import { chatId, mobile, selectedFolder, showSidebar, user } from '$lib/stores';
import {
deleteFolderById,
@@ -703,6 +703,9 @@
createdAt={chat.created_at}
updatedAt={chat.updated_at}
lastReadAt={chat.last_read_at}
ownerName={chat.owner_name ?? null}
ownerUserId={chat.owner_name ? chat.user_id : null}
readonly={chat.user_id !== $user?.id}
{shiftKey}
on:change={(e) => {
dispatch('change', e.detail);
@@ -108,6 +108,8 @@
title={chat.title}
createdAt={chat.created_at}
updatedAt={chat.updated_at}
ownerName={chat.owner_name}
ownerUserId={chat.user_id}
readonly={chat.readonly ?? !isWritable}
/>
{/each}