Quellcode durchsuchen

Merge remote-tracking branch 'upstream/dev' into feat/rag-status

hurxxxx vor 6 Monaten
Ursprung
Commit
857b3f0da8

+ 9 - 8
backend/open_webui/env.py

@@ -356,15 +356,16 @@ WEBUI_SECRET_KEY = os.environ.get(
     ),  # DEPRECATED: remove at next major version
 )
 
-WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
-    "WEBUI_SESSION_COOKIE_SAME_SITE",
-    os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
-)
+WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
 
-WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
-    "WEBUI_SESSION_COOKIE_SECURE",
-    os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
-)
+WEBUI_SESSION_COOKIE_SECURE = os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
+
+WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get("WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE)
+
+WEBUI_AUTH_COOKIE_SECURE = os.environ.get(
+    "WEBUI_AUTH_COOKIE_SECURE", 
+    os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false")
+).lower() == "true"
 
 if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
     raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)

+ 8 - 8
backend/open_webui/routers/auths.py

@@ -25,8 +25,8 @@ from open_webui.env import (
     WEBUI_AUTH,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
-    WEBUI_SESSION_COOKIE_SAME_SITE,
-    WEBUI_SESSION_COOKIE_SECURE,
+    WEBUI_AUTH_COOKIE_SAME_SITE,
+    WEBUI_AUTH_COOKIE_SECURE,
     SRC_LOG_LEVELS,
 )
 from fastapi import APIRouter, Depends, HTTPException, Request, status
@@ -95,8 +95,8 @@ async def get_session_user(
         value=token,
         expires=datetime_expires_at,
         httponly=True,  # Ensures the cookie is not accessible via JavaScript
-        samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-        secure=WEBUI_SESSION_COOKIE_SECURE,
+        samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+        secure=WEBUI_AUTH_COOKIE_SECURE,
     )
 
     user_permissions = get_permissions(
@@ -378,8 +378,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
             value=token,
             expires=datetime_expires_at,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
-            samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-            secure=WEBUI_SESSION_COOKIE_SECURE,
+            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+            secure=WEBUI_AUTH_COOKIE_SECURE,
         )
 
         user_permissions = get_permissions(
@@ -473,8 +473,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 value=token,
                 expires=datetime_expires_at,
                 httponly=True,  # Ensures the cookie is not accessible via JavaScript
-                samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-                secure=WEBUI_SESSION_COOKIE_SECURE,
+                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+                secure=WEBUI_AUTH_COOKIE_SECURE,
             )
 
             if request.app.state.config.WEBHOOK_URL:

+ 30 - 6
backend/open_webui/routers/knowledge.py

@@ -264,7 +264,10 @@ def add_file_to_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -342,7 +345,12 @@ def update_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+        ):
+
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -406,7 +414,11 @@ def remove_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+        ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -484,7 +496,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+        ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -543,7 +559,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+        ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -582,7 +602,11 @@ def add_files_to_knowledge_batch(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+        ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

+ 5 - 1
backend/open_webui/routers/models.py

@@ -183,7 +183,11 @@ async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if model.user_id != user.id and user.role != "admin":
+    if (
+            user.role == "admin"
+            or model.user_id == user.id
+            or has_access(user.id, "write", model.access_control)
+        ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.UNAUTHORIZED,

+ 5 - 1
backend/open_webui/routers/prompts.py

@@ -147,7 +147,11 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if prompt.user_id != user.id and user.role != "admin":
+    if (
+        prompt.user_id != user.id
+        and not has_access(user.id, "write", prompt.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

+ 5 - 1
backend/open_webui/routers/tools.py

@@ -227,7 +227,11 @@ async def delete_tools_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if tools.user_id != user.id and user.role != "admin":
+    if (
+        tools.user_id != user.id
+        and not has_access(user.id, "write", tools.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.UNAUTHORIZED,

+ 1 - 0
backend/open_webui/utils/misc.py

@@ -149,6 +149,7 @@ def openai_chat_chunk_message_template(
         template["choices"][0]["delta"] = {"content": message}
     else:
         template["choices"][0]["finish_reason"] = "stop"
+        template["choices"][0]["delta"] = {}
 
     if usage:
         template["usage"] = usage

+ 5 - 5
backend/open_webui/utils/oauth.py

@@ -35,7 +35,7 @@ from open_webui.config import (
     AppConfig,
 )
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
-from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE
+from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
@@ -323,8 +323,8 @@ class OAuthManager:
             key="token",
             value=jwt_token,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
-            samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-            secure=WEBUI_SESSION_COOKIE_SECURE,
+            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+            secure=WEBUI_AUTH_COOKIE_SECURE,
         )
 
         if ENABLE_OAUTH_SIGNUP.value:
@@ -333,8 +333,8 @@ class OAuthManager:
                 key="oauth_id_token",
                 value=oauth_id_token,
                 httponly=True,
-                samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-                secure=WEBUI_SESSION_COOKIE_SECURE,
+                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+                secure=WEBUI_AUTH_COOKIE_SECURE,
             )
         # Redirect back to the frontend with the JWT token
         redirect_url = f"{request.base_url}auth#token={jwt_token}"

+ 3 - 1
src/lib/components/admin/Users/UserList.svelte

@@ -6,7 +6,9 @@
 
 	import dayjs from 'dayjs';
 	import relativeTime from 'dayjs/plugin/relativeTime';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 	dayjs.extend(relativeTime);
+    dayjs.extend(localizedFormat);
 
 	import { toast } from 'svelte-sonner';
 
@@ -364,7 +366,7 @@
 					</td>
 
 					<td class=" px-3 py-1">
-						{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+						{dayjs(user.created_at * 1000).format('LL')}
 					</td>
 
 					<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>

+ 3 - 1
src/lib/components/admin/Users/UserList/EditUserModal.svelte

@@ -7,9 +7,11 @@
 	import { updateUserById } from '$lib/apis/users';
 
 	import Modal from '$lib/components/common/Modal.svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
+    dayjs.extend(localizedFormat);
 
 	export let show = false;
 	export let selectedUser;
@@ -87,7 +89,7 @@
 
 							<div class="text-xs text-gray-500">
 								{$i18n.t('Created at')}
-								{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+								{dayjs(selectedUser.created_at * 1000).format('LL')}
 							</div>
 						</div>
 					</div>

+ 3 - 1
src/lib/components/admin/Users/UserList/UserChatsModal.svelte

@@ -2,8 +2,10 @@
 	import { toast } from 'svelte-sonner';
 	import dayjs from 'dayjs';
 	import { getContext, createEventDispatcher } from 'svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const dispatch = createEventDispatcher();
+    dayjs.extend(localizedFormat);
 
 	import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
 
@@ -130,7 +132,7 @@
 
 											<td class=" px-3 py-1 hidden md:flex h-[2.5rem] justify-end">
 												<div class="my-auto shrink-0">
-													{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
+													{dayjs(chat.updated_at * 1000).format('LLL')}
 												</div>
 											</td>
 

+ 5 - 3
src/lib/components/channel/Messages/Message.svelte

@@ -3,10 +3,12 @@
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	import isToday from 'dayjs/plugin/isToday';
 	import isYesterday from 'dayjs/plugin/isYesterday';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	dayjs.extend(relativeTime);
 	dayjs.extend(isToday);
 	dayjs.extend(isYesterday);
+    dayjs.extend(localizedFormat);
 
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext<Writable<i18nType>>('i18n');
@@ -154,9 +156,9 @@
 							class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
 						>
 							<Tooltip
-								content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
+								content={dayjs(message.created_at / 1000000).format('LLLL')}
 							>
-								{dayjs(message.created_at / 1000000).format('HH:mm')}
+								{dayjs(message.created_at / 1000000).format('LT')}
 							</Tooltip>
 						</div>
 					{/if}
@@ -175,7 +177,7 @@
 								class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 							>
 								<Tooltip
-									content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
+									content={dayjs(message.created_at / 1000000).format('LLLL')}
 								>
 									<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
 								</Tooltip>

+ 12 - 5
src/lib/components/chat/Chat.svelte

@@ -82,10 +82,12 @@
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import Placeholder from './Placeholder.svelte';
 	import NotificationToast from '../NotificationToast.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 	export let chatIdProp = '';
 
-	let loaded = false;
+	let loading = false;
+
 	const eventTarget = new EventTarget();
 	let controlPane;
 	let controlPaneComponent;
@@ -133,6 +135,7 @@
 
 	$: if (chatIdProp) {
 		(async () => {
+			loading = true;
 			console.log(chatIdProp);
 
 			prompt = '';
@@ -141,11 +144,9 @@
 			webSearchEnabled = false;
 			imageGenerationEnabled = false;
 
-			loaded = false;
-
 			if (chatIdProp && (await loadChat())) {
 				await tick();
-				loaded = true;
+				loading = false;
 
 				if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
 					try {
@@ -1866,7 +1867,7 @@
 		: ' '} w-full max-w-full flex flex-col"
 	id="chat-container"
 >
-	{#if !chatIdProp || (loaded && chatIdProp)}
+	{#if chatIdProp === '' || (!loading && chatIdProp)}
 		{#if $settings?.backgroundImageUrl ?? null}
 			<div
 				class="absolute {$showSidebar
@@ -2070,5 +2071,11 @@
 				{eventTarget}
 			/>
 		</PaneGroup>
+	{:else if loading}
+		<div class=" flex items-center justify-center h-full w-full">
+			<div class="m-auto">
+				<Spinner />
+			</div>
+		</div>
 	{/if}
 </div>

+ 36 - 15
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -48,6 +48,9 @@
 		init();
 	}
 
+	let fileUploadEnabled = true;
+	$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
+
 	const init = async () => {
 		if ($_tools === null) {
 			await _tools.set(await getTools(localStorage.token));
@@ -166,26 +169,44 @@
 			{/if}
 
 			{#if !$mobile}
+				<Tooltip
+					content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+					className="w-full"
+				>
+					<DropdownMenu.Item
+						class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
+							? 'opacity-50'
+							: ''}"
+						on:click={() => {
+							if (fileUploadEnabled) {
+								screenCaptureHandler();
+							}
+						}}
+					>
+						<CameraSolid />
+						<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+					</DropdownMenu.Item>
+				</Tooltip>
+			{/if}
+
+			<Tooltip
+				content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+				className="w-full"
+			>
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl"
+					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
 					on:click={() => {
-						screenCaptureHandler();
+						if (fileUploadEnabled) {
+							uploadFilesHandler();
+						}
 					}}
 				>
-					<CameraSolid />
-					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+					<DocumentArrowUpSolid />
+					<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
 				</DropdownMenu.Item>
-			{/if}
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
-				on:click={() => {
-					uploadFilesHandler();
-				}}
-			>
-				<DocumentArrowUpSolid />
-				<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
-			</DropdownMenu.Item>
+			</Tooltip>
 
 			{#if $config?.features?.enable_google_drive_integration}
 				<DropdownMenu.Item

+ 3 - 1
src/lib/components/chat/Messages/MultiResponseMessages.svelte

@@ -16,7 +16,9 @@
 	import Markdown from './Markdown.svelte';
 	import Name from './Name.svelte';
 	import Skeleton from './Skeleton.svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 	export let chatId;
 	export let history;
@@ -264,7 +266,7 @@
 										<span
 											class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
 										>
-											{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
+											{dayjs(message.timestamp * 1000).format('LT')}
 										</span>
 									{/if}
 								</Name>

+ 1 - 1
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -500,7 +500,7 @@
 					<div
 						class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 					>
-						<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
+						<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
 							<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 						</Tooltip>
 					</div>

+ 3 - 1
src/lib/components/chat/Messages/UserMessage.svelte

@@ -13,8 +13,10 @@
 	import FileItem from '$lib/components/common/FileItem.svelte';
 	import Markdown from './Markdown.svelte';
 	import Image from '$lib/components/common/Image.svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 	export let user;
 
@@ -112,7 +114,7 @@
 						<div
 							class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 						>
-							<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
+							<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
 								<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 							</Tooltip>
 						</div>

+ 3 - 3
src/lib/components/chat/Settings/Personalization/ManageModal.svelte

@@ -11,8 +11,10 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import { error } from '@sveltejs/kit';
 	import EditMemoryModal from './EditMemoryModal.svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 	export let show = false;
 
@@ -84,9 +86,7 @@
 											</td>
 											<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
 												<div class="my-auto whitespace-nowrap">
-													{dayjs(memory.updated_at * 1000).format(
-														$i18n.t('MMMM DD, YYYY hh:mm:ss A')
-													)}
+													{dayjs(memory.updated_at * 1000).format('LLL')}
 												</div>
 											</td>
 											<td class="px-3 py-1">

+ 4 - 1
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte

@@ -4,6 +4,9 @@
 	import { toast } from 'svelte-sonner';
 	import dayjs from 'dayjs';
 	import { getContext, createEventDispatcher } from 'svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
+
+    dayjs.extend(localizedFormat);
 
 	const dispatch = createEventDispatcher();
 
@@ -159,7 +162,7 @@
 
 												<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
 													<div class="my-auto">
-														{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
+														{dayjs(chat.created_at * 1000).format('LLL')}
 													</div>
 												</td>
 

+ 4 - 1
src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte

@@ -112,7 +112,10 @@
 
 		<div class="mt-2">
 			<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
-				<AccessControl bind:accessControl />
+				<AccessControl 
+				bind:accessControl 
+				accessRoles={['read', 'write']}
+				/>
 			</div>
 		</div>
 

+ 4 - 1
src/lib/components/workspace/Models/ModelEditor.svelte

@@ -531,7 +531,10 @@
 
 					<div class="my-2">
 						<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
-							<AccessControl bind:accessControl />
+							<AccessControl 
+							bind:accessControl 
+							accessRoles={['read', 'write']}
+							/>
 						</div>
 					</div>
 

+ 1 - 1
src/lib/i18n/locales/ja-JP/translation.json

@@ -767,7 +767,7 @@
 	"Reset": "",
 	"Reset All Models": "",
 	"Reset Upload Directory": "アップロードディレクトリをリセット",
-	"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジべーうをリセット",
+	"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジベースをリセット",
 	"Reset view": "",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
 	"Response splitting": "応答の分割",

+ 1 - 1
src/lib/i18n/locales/zh-TW/translation.json

@@ -935,7 +935,7 @@
 	"This will delete all models including custom models and cannot be undone.": "這將刪除所有模型,包括自訂模型,且無法復原。",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "這將重設知識庫並同步所有檔案。您確定要繼續嗎?",
 	"Thorough explanation": "詳細解釋",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "{{DURATION}} 思考中",
 	"Tika": "Tika",
 	"Tika Server URL required.": "需要 Tika 伺服器 URL。",
 	"Tiktoken": "Tiktoken",

+ 5 - 3
src/lib/utils/index.ts

@@ -5,10 +5,12 @@ import dayjs from 'dayjs';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import isToday from 'dayjs/plugin/isToday';
 import isYesterday from 'dayjs/plugin/isYesterday';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 dayjs.extend(relativeTime);
 dayjs.extend(isToday);
 dayjs.extend(isYesterday);
+dayjs.extend(localizedFormat);
 
 import { WEBUI_BASE_URL } from '$lib/constants';
 import { TTS_RESPONSE_SPLIT } from '$lib/types';
@@ -295,11 +297,11 @@ export const formatDate = (inputDate) => {
 	const now = dayjs();
 
 	if (date.isToday()) {
-		return `Today at ${date.format('HH:mm')}`;
+		return `Today at ${date.format('LT')}`;
 	} else if (date.isYesterday()) {
-		return `Yesterday at ${date.format('HH:mm')}`;
+		return `Yesterday at ${date.format('LT')}`;
 	} else {
-		return `${date.format('DD/MM/YYYY')} at ${date.format('HH:mm')}`;
+		return `${date.format('L')} at ${date.format('LT')}`;
 	}
 };
 

+ 3 - 1
src/routes/s/[id]/+page.svelte

@@ -16,8 +16,10 @@
 	import { getUserById } from '$lib/apis/users';
 	import { getModels } from '$lib/apis';
 	import { toast } from 'svelte-sonner';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 	let loaded = false;
 
@@ -138,7 +140,7 @@
 
 						<div class="flex text-sm justify-between items-center mt-1">
 							<div class="text-gray-400">
-								{dayjs(chat.chat.timestamp).format($i18n.t('MMMM DD, YYYY'))}
+								{dayjs(chat.chat.timestamp).format('LLL')}
 							</div>
 						</div>
 					</div>