Browse Source

Merge branch 'dev' into fix/share-chat-reactive-loop

Jonathan Rohde 11 months ago
parent
commit
ea6f1a0e57

+ 18 - 0
.github/workflows/format-build-frontend.yaml

@@ -37,3 +37,21 @@ jobs:
 
       - name: Build Frontend
         run: npm run build
+
+  test-frontend:
+    name: 'Frontend Unit Tests'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+
+      - name: Install Dependencies
+        run: npm ci
+
+      - name: Run vitest
+        run: npm run test:frontend

File diff suppressed because it is too large
+ 990 - 6
package-lock.json


+ 4 - 2
package.json

@@ -15,7 +15,8 @@
 		"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
 		"format:backend": "black . --exclude \"/venv/\"",
 		"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
-		"cy:open": "cypress open"
+		"cy:open": "cypress open",
+		"test:frontend": "vitest"
 	},
 	"devDependencies": {
 		"@sveltejs/adapter-auto": "^2.0.0",
@@ -41,7 +42,8 @@
 		"tailwindcss": "^3.3.3",
 		"tslib": "^2.4.1",
 		"typescript": "^5.0.0",
-		"vite": "^4.4.2"
+		"vite": "^4.4.2",
+		"vitest": "^1.6.0"
 	},
 	"type": "module",
 	"dependencies": {

+ 2 - 2
src/lib/components/chat/ModelSelector.svelte

@@ -38,7 +38,7 @@
 	}
 </script>
 
-<div class="flex flex-col mt-0.5 w-full">
+<div class="flex flex-col w-full items-center md:items-start">
 	{#each selectedModels as selectedModel, selectedModelIdx}
 		<div class="flex w-full max-w-fit">
 			<div class="overflow-hidden w-full">
@@ -109,7 +109,7 @@
 </div>
 
 {#if showSetDefault}
-	<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
+	<div class="hidden md:absolute text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
 		<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
 	</div>
 {/if}

+ 6 - 5
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -10,7 +10,7 @@
 
 	import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
 
-	import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
+	import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
 	import { toast } from 'svelte-sonner';
 	import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -201,10 +201,11 @@
 			<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
 		</div>
 	</DropdownMenu.Trigger>
+
 	<DropdownMenu.Content
-		class=" z-40 {className} max-w-[calc(100vw-1rem)] justify-start rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none "
+		class=" z-40 {className} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none "
 		transition={flyAndScale}
-		side={'bottom-start'}
+		side={$mobile ? 'bottom' : 'bottom-start'}
 		sideOffset={4}
 	>
 		<slot>
@@ -228,7 +229,7 @@
 				{#each filteredItems as item}
 					<button
 						aria-label="model-item"
-						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
 						on:click={() => {
 							value = item.value;
 
@@ -312,7 +313,7 @@
 
 				{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
 					<button
-						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
 						on:click={() => {
 							pullModelHandler();
 						}}

+ 68 - 7
src/lib/components/layout/Navbar.svelte

@@ -2,7 +2,16 @@
 	import { getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 
-	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
+	import {
+		WEBUI_NAME,
+		chatId,
+		mobile,
+		modelfiles,
+		settings,
+		showSettings,
+		showSidebar,
+		user
+	} from '$lib/stores';
 
 	import { slide } from 'svelte/transition';
 	import ShareChatModal from '../chat/ShareChatModal.svelte';
@@ -10,6 +19,7 @@
 	import Tooltip from '../common/Tooltip.svelte';
 	import Menu from './Navbar/Menu.svelte';
 	import { page } from '$app/stores';
+	import UserMenu from './Sidebar/UserMenu.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -28,8 +38,34 @@
 
 <ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
 <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
-	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1.3rem]">
+	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
 		<div class="flex items-center w-full max-w-full">
+			<div class="{$showSidebar ? 'md:hidden' : ''} mr-3 self-start flex flex-none items-center">
+				<button
+					id="sidebar-toggle-button"
+					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+					on:click={() => {
+						showSidebar.set(!$showSidebar);
+					}}
+				>
+					<div class=" m-auto self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="2"
+							stroke="currentColor"
+							class="size-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
+							/>
+						</svg>
+					</div>
+				</button>
+			</div>
 			<div class="flex-1 overflow-hidden max-w-full">
 				{#if showModelSelector}
 					<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
@@ -37,12 +73,12 @@
 			</div>
 
 			<div class="self-start flex flex-none items-center">
-				<div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
+				<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
 
 				{#if !shareEnabled}
 					<Tooltip content={$i18n.t('Settings')}>
 						<button
-							class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
+							class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 							id="open-settings-button"
 							on:click={async () => {
 								await showSettings.set(!$showSettings);
@@ -81,9 +117,9 @@
 						}}
 					>
 						<button
-							class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
+							class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 							id="chat-context-menu-button"
-						>
+            >
 							<div class=" m-auto self-center">
 								<svg
 									xmlns="http://www.w3.org/2000/svg"
@@ -106,7 +142,9 @@
 				<Tooltip content={$i18n.t('New Chat')}>
 					<button
 						id="new-chat-button"
-						class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
+						class=" flex {$showSidebar
+							? 'md:hidden'
+							: ''} cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 						on:click={() => {
 							initNewChat();
 						}}
@@ -128,6 +166,29 @@
 						</div>
 					</button>
 				</Tooltip>
+
+				{#if !$mobile && $user !== undefined}
+					<UserMenu
+						role={$user.role}
+						on:show={(e) => {
+							if (e.detail === 'archived-chat') {
+								// showArchivedChatsModal = true;
+							}
+						}}
+					>
+						<button
+							class=" flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+						>
+							<div class=" self-center">
+								<img
+									src={$user.profile_image_url}
+									class=" size-6 object-cover rounded-full"
+									alt="User profile"
+								/>
+							</div>
+						</button>
+					</UserMenu>
+				{/if}
 			</div>
 		</div>
 	</div>

+ 7 - 7
src/lib/components/layout/Navbar/Menu.svelte

@@ -76,14 +76,14 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
+			class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
 			sideOffset={8}
 			side="bottom"
 			align="end"
 			transition={flyAndScale}
 		>
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 				on:click={async () => {
 					await showSettings.set(!$showSettings);
 				}}
@@ -112,7 +112,7 @@
 
 			{#if shareEnabled}
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 					id="chat-share-button"
 					on:click={() => {
 						shareHandler();
@@ -141,7 +141,7 @@
 				/> -->
 				<DropdownMenu.Sub>
 					<DropdownMenu.SubTrigger
-						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 					>
 						<svg
 							xmlns="http://www.w3.org/2000/svg"
@@ -161,12 +161,12 @@
 						<div class="flex items-center">{$i18n.t('Download')}</div>
 					</DropdownMenu.SubTrigger>
 					<DropdownMenu.SubContent
-						class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
+						class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
 						transition={flyAndScale}
 						sideOffset={8}
 					>
 						<DropdownMenu.Item
-							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 							on:click={() => {
 								downloadTxt();
 							}}
@@ -175,7 +175,7 @@
 						</DropdownMenu.Item>
 
 						<DropdownMenu.Item
-							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-800 rounded-md"
 							on:click={() => {
 								downloadPdf();
 							}}

+ 80 - 47
src/lib/components/layout/Sidebar.svelte

@@ -1,6 +1,15 @@
 <script lang="ts">
 	import { goto } from '$app/navigation';
-	import { user, chats, settings, showSettings, chatId, tags, showSidebar } from '$lib/stores';
+	import {
+		user,
+		chats,
+		settings,
+		showSettings,
+		chatId,
+		tags,
+		showSidebar,
+		mobile
+	} from '$lib/stores';
 	import { onMount, getContext } from 'svelte';
 
 	const i18n = getContext('i18n');
@@ -183,6 +192,17 @@
 	}}
 />
 
+<!-- svelte-ignore a11y-no-static-element-interactions -->
+
+{#if $showSidebar}
+	<div
+		class=" fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain"
+		on:mousedown={() => {
+			showSidebar.set(!$showSidebar);
+		}}
+	/>
+{/if}
+
 <div
 	bind:this={navElement}
 	id="sidebar"
@@ -193,14 +213,37 @@
 	data-state={$showSidebar}
 >
 	<div
-		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {$showSidebar
+		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
 			? ''
 			: 'invisible'}"
 	>
-		<div class="px-2 flex justify-center space-x-2">
+		<div class="px-2 flex justify-between space-x-2">
+			<button
+				class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+				on:click={() => {
+					showSidebar.set(!$showSidebar);
+				}}
+			>
+				<div class=" m-auto self-center">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="2"
+						stroke="currentColor"
+						class="size-5"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
+						/>
+					</svg>
+				</div>
+			</button>
 			<a
 				id="sidebar-new-chat-button"
-				class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				class="flex justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 				href="/"
 				on:click={async () => {
 					selectedChatId = null;
@@ -212,24 +255,12 @@
 					}, 0);
 				}}
 			>
-				<div class="flex self-center">
-					<div class="self-center mr-1.5">
-						<img
-							src="{WEBUI_BASE_URL}/static/favicon.png"
-							class=" size-6 -translate-x-1.5 rounded-full"
-							alt="logo"
-						/>
-					</div>
-
-					<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
-				</div>
-
 				<div class="self-center">
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
 						viewBox="0 0 20 20"
 						fill="currentColor"
-						class="w-4 h-4"
+						class="size-5"
 					>
 						<path
 							d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
@@ -681,43 +712,45 @@
 			</div>
 		</div>
 
-		<div class="px-2.5">
-			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
-
-			<div class="flex flex-col">
-				{#if $user !== undefined}
-					<UserMenu
-						role={$user.role}
-						on:show={(e) => {
-							if (e.detail === 'archived-chat') {
-								showArchivedChatsModal = true;
-							}
-						}}
-					>
-						<button
-							class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-							on:click={() => {
-								showDropdown = !showDropdown;
+		{#if $mobile}
+			<div class="px-2.5">
+				<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
+
+				<div class="flex flex-col">
+					{#if $user !== undefined}
+						<UserMenu
+							role={$user.role}
+							on:show={(e) => {
+								if (e.detail === 'archived-chat') {
+									showArchivedChatsModal = true;
+								}
 							}}
 						>
-							<div class=" self-center mr-3">
-								<img
-									src={$user.profile_image_url}
-									class=" max-w-[30px] object-cover rounded-full"
-									alt="User profile"
-								/>
-							</div>
-							<div class=" self-center font-semibold">{$user.name}</div>
-						</button>
-					</UserMenu>
-				{/if}
+							<button
+								class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+								on:click={() => {
+									showDropdown = !showDropdown;
+								}}
+							>
+								<div class=" self-center mr-3">
+									<img
+										src={$user.profile_image_url}
+										class=" max-w-[30px] object-cover rounded-full"
+										alt="User profile"
+									/>
+								</div>
+								<div class=" self-center font-semibold">{$user.name}</div>
+							</button>
+						</UserMenu>
+					{/if}
+				</div>
 			</div>
-		</div>
+		{/if}
 	</div>
 
 	<div
 		id="sidebar-handle"
-		class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
+		class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
 	>
 		<Tooltip
 			placement="right"

+ 4 - 4
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -36,14 +36,14 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
+			class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
 			sideOffset={-2}
 			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 					shareHandler();
 				}}
@@ -53,7 +53,7 @@
 			</DropdownMenu.Item>
 
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 					renameHandler();
 				}}
@@ -63,7 +63,7 @@
 			</DropdownMenu.Item>
 
 			<DropdownMenu.Item
-				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
+				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 					deleteHandler();
 				}}

+ 1 - 1
src/lib/components/layout/Sidebar/UserMenu.svelte

@@ -28,7 +28,7 @@
 
 	<slot name="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[240px] rounded-lg p-1 py-1 border border-gray-850 z-50 bg-gray-900 text-white text-sm"
+			class="w-full max-w-[240px] rounded-lg p-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-gray-850 text-white text-sm"
 			sideOffset={8}
 			side="bottom"
 			align="start"

+ 2 - 2
src/lib/constants.ts

@@ -1,8 +1,8 @@
-import { dev } from '$app/environment';
+import { browser, dev } from '$app/environment';
 // import { version } from '../../package.json';
 
 export const APP_NAME = 'Open WebUI';
-export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
+export const WEBUI_BASE_URL = browser ? (dev ? `http://${location.hostname}:8080` : ``) : ``;
 
 export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
 

+ 137 - 137
src/lib/i18n/locales/it-IT/translation.json

@@ -1,34 +1,34 @@
 {
 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' per nessuna scadenza.",
 	"(Beta)": "(Beta)",
-	"(e.g. `sh webui.sh --api`)": "",
+	"(e.g. `sh webui.sh --api`)": "(p.e. `sh webui.sh --api`)",
 	"(latest)": "(ultima)",
 	"{{modelName}} is thinking...": "{{modelName}} sta pensando...",
-	"{{user}}'s Chats": "",
+	"{{user}}'s Chats": "{{user}} Chat",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend richiesto",
 	"a user": "un utente",
 	"About": "Informazioni",
 	"Account": "Account",
-	"Accurate information": "",
+	"Accurate information": "Informazioni accurate",
 	"Add a model": "Aggiungi un modello",
 	"Add a model tag name": "Aggiungi un nome tag del modello",
 	"Add a short description about what this modelfile does": "Aggiungi una breve descrizione di ciò che fa questo file modello",
 	"Add a short title for this prompt": "Aggiungi un titolo breve per questo prompt",
 	"Add a tag": "Aggiungi un tag",
-	"Add custom prompt": "",
+	"Add custom prompt": "Aggiungi un prompt custom",
 	"Add Docs": "Aggiungi documenti",
 	"Add Files": "Aggiungi file",
 	"Add message": "Aggiungi messaggio",
-	"Add Model": "",
-	"Add Tags": "aggiungi tag",
-	"Add User": "",
+	"Add Model": "Aggiungi modello",
+	"Add Tags": "Aggiungi tag",
+	"Add User": "Aggiungi utente",
 	"Adjusting these settings will apply changes universally to all users.": "La modifica di queste impostazioni applicherà le modifiche universalmente a tutti gli utenti.",
 	"admin": "amministratore",
 	"Admin Panel": "Pannello di amministrazione",
 	"Admin Settings": "Impostazioni amministratore",
 	"Advanced Parameters": "Parametri avanzati",
 	"all": "tutti",
-	"All Documents": "",
+	"All Documents": "Tutti i documenti",
 	"All Users": "Tutti gli utenti",
 	"Allow": "Consenti",
 	"Allow Chat Deletion": "Consenti l'eliminazione della chat",
@@ -36,32 +36,32 @@
 	"Already have an account?": "Hai già un account?",
 	"an assistant": "un assistente",
 	"and": "e",
-	"and create a new shared link.": "",
+	"and create a new shared link.": "e crea un nuovo link condiviso.",
 	"API Base URL": "URL base API",
 	"API Key": "Chiave API",
-	"API Key created.": "",
-	"API keys": "",
+	"API Key created.": "Chiave API creata.",
+	"API keys": "Chiavi API",
 	"API RPM": "API RPM",
-	"April": "",
-	"Archive": "",
+	"April": "Aprile",
+	"Archive": "Archivio",
 	"Archived Chats": "Chat archiviate",
 	"are allowed - Activate this command by typing": "sono consentiti - Attiva questo comando digitando",
 	"Are you sure?": "Sei sicuro?",
-	"Attach file": "",
-	"Attention to detail": "",
+	"Attach file": "Allega file",
+	"Attention to detail": "Attenzione ai dettagli",
 	"Audio": "Audio",
-	"August": "",
+	"August": "Agosto",
 	"Auto-playback response": "Riproduzione automatica della risposta",
 	"Auto-send input after 3 sec.": "Invio automatico dell'input dopo 3 secondi.",
 	"AUTOMATIC1111 Base URL": "URL base AUTOMATIC1111",
 	"AUTOMATIC1111 Base URL is required.": "L'URL base AUTOMATIC1111 è obbligatorio.",
 	"available!": "disponibile!",
 	"Back": "Indietro",
-	"Bad Response": "",
-	"before": "",
-	"Being lazy": "",
+	"Bad Response": "Risposta non valida",
+	"before": "prima",
+	"Being lazy": "Essere pigri",
 	"Builder Mode": "Modalità costruttore",
-	"Bypass SSL verification for Websites": "",
+	"Bypass SSL verification for Websites": "Aggira la verifica SSL per i siti web",
 	"Cancel": "Annulla",
 	"Categories": "Categorie",
 	"Change Password": "Cambia password",
@@ -78,66 +78,66 @@
 	"Chunk Size": "Dimensione chunk",
 	"Citation": "Citazione",
 	"Click here for help.": "Clicca qui per aiuto.",
-	"Click here to": "",
+	"Click here to": "Clicca qui per",
 	"Click here to check other modelfiles.": "Clicca qui per controllare altri file modello.",
 	"Click here to select": "Clicca qui per selezionare",
-	"Click here to select a csv file.": "",
+	"Click here to select a csv file.": "Clicca qui per selezionare un file csv.",
 	"Click here to select documents.": "Clicca qui per selezionare i documenti.",
 	"click here.": "clicca qui.",
 	"Click on the user role button to change a user's role.": "Clicca sul pulsante del ruolo utente per modificare il ruolo di un utente.",
 	"Close": "Chiudi",
 	"Collection": "Collezione",
-	"ComfyUI": "",
-	"ComfyUI Base URL": "",
-	"ComfyUI Base URL is required.": "",
+	"ComfyUI": "ComfyUI",
+	"ComfyUI Base URL": "URL base ComfyUI",
+	"ComfyUI Base URL is required.": "L'URL base ComfyUI è obbligatorio.",
 	"Command": "Comando",
 	"Confirm Password": "Conferma password",
 	"Connections": "Connessioni",
 	"Content": "Contenuto",
 	"Context Length": "Lunghezza contesto",
-	"Continue Response": "",
+	"Continue Response": "Continua risposta",
 	"Conversation Mode": "Modalità conversazione",
-	"Copied shared chat URL to clipboard!": "",
-	"Copy": "",
+	"Copied shared chat URL to clipboard!": "URL della chat condivisa copiato negli appunti!",
+	"Copy": "Copia",
 	"Copy last code block": "Copia ultimo blocco di codice",
 	"Copy last response": "Copia ultima risposta",
-	"Copy Link": "",
+	"Copy Link": "Copia link",
 	"Copying to clipboard was successful!": "Copia negli appunti riuscita!",
 	"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Crea una frase concisa di 3-5 parole come intestazione per la seguente query, aderendo rigorosamente al limite di 3-5 parole ed evitando l'uso della parola 'titolo':",
 	"Create a modelfile": "Crea un file modello",
 	"Create Account": "Crea account",
-	"Create new key": "",
-	"Create new secret key": "",
+	"Create new key": "Crea nuova chiave",
+	"Create new secret key": "Crea nuova chiave segreta",
 	"Created at": "Creato il",
-	"Created At": "",
+	"Created At": "Creato il",
 	"Current Model": "Modello corrente",
 	"Current Password": "Password corrente",
 	"Custom": "Personalizzato",
 	"Customize Ollama models for a specific purpose": "Personalizza i modelli Ollama per uno scopo specifico",
 	"Dark": "Scuro",
-	"Dashboard": "",
+	"Dashboard": "Pannello di controllo",
 	"Database": "Database",
 	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
-	"December": "",
+	"December": "Dicembre",
 	"Default": "Predefinito",
 	"Default (Automatic1111)": "Predefinito (Automatic1111)",
-	"Default (SentenceTransformers)": "",
+	"Default (SentenceTransformers)": "Predefinito (SentenceTransformers)",
 	"Default (Web API)": "Predefinito (API Web)",
 	"Default model updated": "Modello predefinito aggiornato",
 	"Default Prompt Suggestions": "Suggerimenti prompt predefiniti",
 	"Default User Role": "Ruolo utente predefinito",
 	"delete": "elimina",
-	"Delete": "",
+	"Delete": "Elimina",
 	"Delete a model": "Elimina un modello",
 	"Delete chat": "Elimina chat",
-	"Delete Chat": "",
-	"Delete Chats": "Elimina chat",
-	"delete this link": "",
-	"Delete User": "",
+	"Delete Chat": "Elimina chat",
+	"Delete Chats": "Elimina le chat",
+	"delete this link": "elimina questo link",
+	"Delete User": "Elimina utente",
 	"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}",
-	"Deleted {{tagName}}": "",
+	"Deleted {{tagName}}": "Eliminato {{tagName}}",
 	"Description": "Descrizione",
-	"Didn't fully follow instructions": "",
+	"Didn't fully follow instructions": "Non ha seguito completamente le istruzioni",
 	"Disabled": "Disabilitato",
 	"Discover a modelfile": "Scopri un file modello",
 	"Discover a prompt": "Scopri un prompt",
@@ -150,28 +150,28 @@
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "non effettua connessioni esterne e i tuoi dati rimangono al sicuro sul tuo server ospitato localmente.",
 	"Don't Allow": "Non consentire",
 	"Don't have an account?": "Non hai un account?",
-	"Don't like the style": "",
-	"Download": "",
-	"Download canceled": "",
+	"Don't like the style": "Non ti piace lo stile",
+	"Download": "Scarica",
+	"Download canceled": "Scaricamento annullato",
 	"Download Database": "Scarica database",
 	"Drop any files here to add to the conversation": "Trascina qui i file da aggiungere alla conversazione",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "ad esempio '30s','10m'. Le unità di tempo valide sono 's', 'm', 'h'.",
-	"Edit": "",
+	"Edit": "Modifica",
 	"Edit Doc": "Modifica documento",
 	"Edit User": "Modifica utente",
 	"Email": "Email",
-	"Embedding Model": "",
-	"Embedding Model Engine": "",
-	"Embedding model set to \"{{embedding_model}}\"": "",
+	"Embedding Model": "Modello di embedding",
+	"Embedding Model Engine": "Motore del modello di embedding",
+	"Embedding model set to \"{{embedding_model}}\"": "Modello di embedding impostato su \"{{embedding_model}}\"",
 	"Enable Chat History": "Abilita cronologia chat",
 	"Enable New Sign Ups": "Abilita nuove iscrizioni",
 	"Enabled": "Abilitato",
-	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
+	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assicurati che il tuo file CSV includa 4 colonne in questo ordine: Nome, Email, Password, Ruolo.",
 	"Enter {{role}} message here": "Inserisci il messaggio per {{role}} qui",
 	"Enter Chunk Overlap": "Inserisci la sovrapposizione chunk",
 	"Enter Chunk Size": "Inserisci la dimensione chunk",
 	"Enter Image Size (e.g. 512x512)": "Inserisci la dimensione dell'immagine (ad esempio 512x512)",
-	"Enter language codes": "",
+	"Enter language codes": "Inserisci i codici lingua",
 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "Inserisci l'URL base dell'API LiteLLM (litellm_params.api_base)",
 	"Enter LiteLLM API Key (litellm_params.api_key)": "Inserisci la chiave API LiteLLM (litellm_params.api_key)",
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Inserisci LiteLLM API RPM (litellm_params.rpm)",
@@ -179,45 +179,45 @@
 	"Enter Max Tokens (litellm_params.max_tokens)": "Inserisci Max Tokens (litellm_params.max_tokens)",
 	"Enter model tag (e.g. {{modelTag}})": "Inserisci il tag del modello (ad esempio {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Inserisci il numero di passaggi (ad esempio 50)",
-	"Enter Score": "",
+	"Enter Score": "Inserisci il punteggio",
 	"Enter stop sequence": "Inserisci la sequenza di arresto",
 	"Enter Top K": "Inserisci Top K",
 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Inserisci URL (ad esempio http://127.0.0.1:7860/)",
-	"Enter URL (e.g. http://localhost:11434)": "",
+	"Enter URL (e.g. http://localhost:11434)": "Inserisci URL (ad esempio http://localhost:11434)",
 	"Enter Your Email": "Inserisci la tua email",
 	"Enter Your Full Name": "Inserisci il tuo nome completo",
 	"Enter Your Password": "Inserisci la tua password",
-	"Enter Your Role": "",
+	"Enter Your Role": "Inserisci il tuo ruolo",
 	"Experimental": "Sperimentale",
 	"Export All Chats (All Users)": "Esporta tutte le chat (tutti gli utenti)",
 	"Export Chats": "Esporta chat",
 	"Export Documents Mapping": "Esporta mappatura documenti",
 	"Export Modelfiles": "Esporta file modello",
 	"Export Prompts": "Esporta prompt",
-	"Failed to create API Key.": "",
+	"Failed to create API Key.": "Impossibile creare la chiave API.",
 	"Failed to read clipboard contents": "Impossibile leggere il contenuto degli appunti",
-	"February": "",
-	"Feel free to add specific details": "",
+	"February": "Febbraio",
+	"Feel free to add specific details": "Sentiti libero/a di aggiungere dettagli specifici",
 	"File Mode": "Modalità file",
 	"File not found.": "File non trovato.",
-	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
+	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Rilevato spoofing delle impronte digitali: impossibile utilizzare le iniziali come avatar. Ripristino all'immagine del profilo predefinita.",
 	"Fluidly stream large external response chunks": "Trasmetti in modo fluido blocchi di risposta esterni di grandi dimensioni",
 	"Focus chat input": "Metti a fuoco l'input della chat",
-	"Followed instructions perfectly": "",
+	"Followed instructions perfectly": "Ha seguito le istruzioni alla perfezione",
 	"Format your variables using square brackets like this:": "Formatta le tue variabili usando parentesi quadre come questa:",
 	"From (Base Model)": "Da (modello base)",
 	"Full Screen Mode": "Modalità a schermo intero",
 	"General": "Generale",
 	"General Settings": "Impostazioni generali",
-	"Generation Info": "",
-	"Good Response": "",
-	"has no conversations.": "",
+	"Generation Info": "Informazioni generazione",
+	"Good Response": "Buona risposta",
+	"has no conversations.": "non ha conversazioni.",
 	"Hello, {{name}}": "Ciao, {{name}}",
-	"Help": "",
+	"Help": "Aiuto",
 	"Hide": "Nascondi",
 	"Hide Additional Params": "Nascondi parametri aggiuntivi",
 	"How can I help you today?": "Come posso aiutarti oggi?",
-	"Hybrid Search": "",
+	"Hybrid Search": "Ricerca ibrida",
 	"Image Generation (Experimental)": "Generazione di immagini (sperimentale)",
 	"Image Generation Engine": "Motore di generazione immagini",
 	"Image Settings": "Impostazioni immagine",
@@ -226,21 +226,21 @@
 	"Import Documents Mapping": "Importa mappatura documenti",
 	"Import Modelfiles": "Importa file modello",
 	"Import Prompts": "Importa prompt",
-	"Include `--api` flag when running stable-diffusion-webui": "",
-	"Input commands": "",
+	"Include `--api` flag when running stable-diffusion-webui": "Includi il flag `--api` quando esegui stable-diffusion-webui",
+	"Input commands": "Comandi di input",
 	"Interface": "Interfaccia",
-	"Invalid Tag": "",
-	"January": "",
+	"Invalid Tag": "Tag non valido",
+	"January": "Gennaio",
 	"join our Discord for help.": "unisciti al nostro Discord per ricevere aiuto.",
 	"JSON": "JSON",
-	"July": "",
-	"June": "",
+	"July": "Luglio",
+	"June": "Giugno",
 	"JWT Expiration": "Scadenza JWT",
 	"JWT Token": "Token JWT",
 	"Keep Alive": "Mantieni attivo",
 	"Keyboard shortcuts": "Scorciatoie da tastiera",
 	"Language": "Lingua",
-	"Last Active": "",
+	"Last Active": "Ultima attività",
 	"Light": "Chiaro",
 	"Listening...": "Ascolto...",
 	"LLMs can make mistakes. Verify important information.": "Gli LLM possono commettere errori. Verifica le informazioni importanti.",
@@ -249,22 +249,22 @@
 	"Manage LiteLLM Models": "Gestisci modelli LiteLLM",
 	"Manage Models": "Gestisci modelli",
 	"Manage Ollama Models": "Gestisci modelli Ollama",
-	"March": "",
+	"March": "Marzo",
 	"Max Tokens": "Max token",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "È possibile scaricare un massimo di 3 modelli contemporaneamente. Riprova più tardi.",
-	"May": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "",
-	"Minimum Score": "",
+	"May": "Maggio",
+	"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat.": "I messaggi che invii dopo aver creato il tuo link non verranno condivisi. Gli utenti con l'URL potranno visualizzare la chat condivisa.",
+	"Minimum Score": "Punteggio minimo",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
 	"Mirostat Tau": "Mirostat Tau",
 	"MMMM DD, YYYY": "MMMM DD, YYYY",
-	"MMMM DD, YYYY HH:mm": "",
+	"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
 	"Model '{{modelName}}' has been successfully downloaded.": "Il modello '{{modelName}}' è stato scaricato con successo.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Il modello '{{modelTag}}' è già in coda per il download.",
 	"Model {{modelId}} not found": "Modello {{modelId}} non trovato",
 	"Model {{modelName}} already exists.": "Il modello {{modelName}} esiste già.",
-	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
+	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Percorso del filesystem del modello rilevato. Il nome breve del modello è richiesto per l'aggiornamento, impossibile continuare.",
 	"Model Name": "Nome modello",
 	"Model not selected": "Modello non selezionato",
 	"Model Tag Name": "Nome tag del modello",
@@ -275,7 +275,7 @@
 	"Modelfile Content": "Contenuto del file modello",
 	"Modelfiles": "File modello",
 	"Models": "Modelli",
-	"More": "",
+	"More": "Altro",
 	"My Documents": "I miei documenti",
 	"My Modelfiles": "I miei file modello",
 	"My Prompts": "I miei prompt",
@@ -284,19 +284,19 @@
 	"Name your modelfile": "Assegna un nome al tuo file modello",
 	"New Chat": "Nuova chat",
 	"New Password": "Nuova password",
-	"No results found": "",
+	"No results found": "Nessun risultato trovato",
 	"No source available": "Nessuna fonte disponibile",
-	"Not factually correct": "",
+	"Not factually correct": "Non corretto dal punto di vista fattuale",
 	"Not sure what to add?": "Non sei sicuro di cosa aggiungere?",
 	"Not sure what to write? Switch to": "Non sei sicuro di cosa scrivere? Passa a",
-	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "",
+	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: se imposti un punteggio minimo, la ricerca restituirà solo i documenti con un punteggio maggiore o uguale al punteggio minimo.",
 	"Notifications": "Notifiche desktop",
-	"November": "",
-	"October": "",
+	"November": "Novembre",
+	"October": "Ottobre",
 	"Off": "Disattivato",
 	"Okay, Let's Go!": "Ok, andiamo!",
-	"OLED Dark": "",
-	"Ollama": "",
+	"OLED Dark": "OLED scuro",
+	"Ollama": "Ollama",
 	"Ollama Base URL": "URL base Ollama",
 	"Ollama Version": "Versione Ollama",
 	"On": "Attivato",
@@ -309,52 +309,52 @@
 	"Open AI": "Open AI",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
 	"Open new chat": "Apri nuova chat",
-	"OpenAI": "",
+	"OpenAI": "OpenAI",
 	"OpenAI API": "API OpenAI",
-	"OpenAI API Config": "",
+	"OpenAI API Config": "Configurazione API OpenAI",
 	"OpenAI API Key is required.": "La chiave API OpenAI è obbligatoria.",
-	"OpenAI URL/Key required.": "",
+	"OpenAI URL/Key required.": "URL/Chiave OpenAI obbligatori.",
 	"or": "o",
-	"Other": "",
-	"Overview": "",
+	"Other": "Altro",
+	"Overview": "Panoramica",
 	"Parameters": "Parametri",
 	"Password": "Password",
-	"PDF document (.pdf)": "",
+	"PDF document (.pdf)": "Documento PDF (.pdf)",
 	"PDF Extract Images (OCR)": "Estrazione immagini PDF (OCR)",
 	"pending": "in sospeso",
 	"Permission denied when accessing microphone: {{error}}": "Autorizzazione negata durante l'accesso al microfono: {{error}}",
-	"Plain text (.txt)": "",
+	"Plain text (.txt)": "Testo normale (.txt)",
 	"Playground": "Terreno di gioco",
-	"Positive attitude": "",
-	"Previous 30 days": "",
-	"Previous 7 days": "",
-	"Profile Image": "",
-	"Prompt": "",
-	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "",
+	"Positive attitude": "Attitudine positiva",
+	"Previous 30 days": "Ultimi 30 giorni",
+	"Previous 7 days": "Ultimi 7 giorni",
+	"Profile Image": "Immagine del profilo",
+	"Prompt": "Prompt",
+	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (ad esempio Dimmi un fatto divertente sull'Impero Romano)",
 	"Prompt Content": "Contenuto del prompt",
 	"Prompt suggestions": "Suggerimenti prompt",
 	"Prompts": "Prompt",
-	"Pull \"{{searchValue}}\" from Ollama.com": "",
+	"Pull \"{{searchValue}}\" from Ollama.com": "Estrai \"{{searchValue}}\" da Ollama.com",
 	"Pull a model from Ollama.com": "Estrai un modello da Ollama.com",
 	"Pull Progress": "Avanzamento estrazione",
 	"Query Params": "Parametri query",
 	"RAG Template": "Modello RAG",
 	"Raw Format": "Formato raw",
-	"Read Aloud": "",
+	"Read Aloud": "Leggi ad alta voce",
 	"Record voice": "Registra voce",
 	"Redirecting you to OpenWebUI Community": "Reindirizzamento alla comunità OpenWebUI",
-	"Refused when it shouldn't have": "",
-	"Regenerate": "",
+	"Refused when it shouldn't have": "Rifiutato quando non avrebbe dovuto",
+	"Regenerate": "Rigenera",
 	"Release Notes": "Note di rilascio",
-	"Remove": "",
-	"Remove Model": "",
-	"Rename": "",
+	"Remove": "Rimuovi",
+	"Remove Model": "Rimuovi modello",
+	"Rename": "Rinomina",
 	"Repeat Last N": "Ripeti ultimi N",
 	"Repeat Penalty": "Penalità di ripetizione",
 	"Request Mode": "Modalità richiesta",
-	"Reranking Model": "",
-	"Reranking model disabled": "",
-	"Reranking model set to \"{{reranking_model}}\"": "",
+	"Reranking Model": "Modello di riclassificazione",
+	"Reranking model disabled": "Modello di riclassificazione disabilitato",
+	"Reranking model set to \"{{reranking_model}}\"": "Modello di riclassificazione impostato su \"{{reranking_model}}\"",
 	"Reset Vector Storage": "Reimposta archivio vettoriale",
 	"Response AutoCopy to Clipboard": "Copia automatica della risposta negli appunti",
 	"Role": "Ruolo",
@@ -369,7 +369,7 @@
 	"Scan complete!": "Scansione completata!",
 	"Scan for documents from {{path}}": "Cerca documenti da {{path}}",
 	"Search": "Cerca",
-	"Search a model": "",
+	"Search a model": "Cerca un modello",
 	"Search Documents": "Cerca documenti",
 	"Search Prompts": "Cerca prompt",
 	"See readme.md for instructions": "Vedi readme.md per le istruzioni",
@@ -378,35 +378,35 @@
 	"Select a mode": "Seleziona una modalità",
 	"Select a model": "Seleziona un modello",
 	"Select an Ollama instance": "Seleziona un'istanza Ollama",
-	"Select model": "",
+	"Select model": "Seleziona modello",
 	"Send a Message": "Invia un messaggio",
 	"Send message": "Invia messaggio",
-	"September": "",
+	"September": "Settembre",
 	"Server connection verified": "Connessione al server verificata",
 	"Set as default": "Imposta come predefinito",
 	"Set Default Model": "Imposta modello predefinito",
-	"Set embedding model (e.g. {{model}})": "",
+	"Set embedding model (e.g. {{model}})": "Imposta modello di embedding (ad esempio {{model}})",
 	"Set Image Size": "Imposta dimensione immagine",
 	"Set Model": "Imposta modello",
-	"Set reranking model (e.g. {{model}})": "",
+	"Set reranking model (e.g. {{model}})": "Imposta modello di riclassificazione (ad esempio {{model}})",
 	"Set Steps": "Imposta passaggi",
 	"Set Title Auto-Generation Model": "Imposta modello di generazione automatica del titolo",
 	"Set Voice": "Imposta voce",
 	"Settings": "Impostazioni",
 	"Settings saved successfully!": "Impostazioni salvate con successo!",
-	"Share": "",
-	"Share Chat": "",
+	"Share": "Condividi",
+	"Share Chat": "Condividi chat",
 	"Share to OpenWebUI Community": "Condividi con la comunità OpenWebUI",
 	"short-summary": "riassunto-breve",
 	"Show": "Mostra",
 	"Show Additional Params": "Mostra parametri aggiuntivi",
 	"Show shortcuts": "Mostra",
-	"Showcased creativity": "",
+	"Showcased creativity": "Creatività messa in mostra",
 	"sidebar": "barra laterale",
 	"Sign in": "Accedi",
 	"Sign Out": "Esci",
 	"Sign up": "Registrati",
-	"Signing in": "",
+	"Signing in": "Accesso in corso",
 	"Source": "Fonte",
 	"Speech recognition error: {{error}}": "Errore di riconoscimento vocale: {{error}}",
 	"Speech-to-Text Engine": "Motore da voce a testo",
@@ -414,37 +414,37 @@
 	"Stop Sequence": "Sequenza di arresto",
 	"STT Settings": "Impostazioni STT",
 	"Submit": "Invia",
-	"Subtitle (e.g. about the Roman Empire)": "",
+	"Subtitle (e.g. about the Roman Empire)": "Sottotitolo (ad esempio sull'Impero Romano)",
 	"Success": "Successo",
 	"Successfully updated.": "Aggiornato con successo.",
-	"Suggested": "",
+	"Suggested": "Suggerito",
 	"Sync All": "Sincronizza tutto",
 	"System": "Sistema",
 	"System Prompt": "Prompt di sistema",
 	"Tags": "Tag",
-	"Tell us more:": "",
+	"Tell us more:": "Raccontaci di più:",
 	"Temperature": "Temperatura",
 	"Template": "Modello",
 	"Text Completion": "Completamento del testo",
 	"Text-to-Speech Engine": "Motore da testo a voce",
 	"Tfs Z": "Tfs Z",
-	"Thanks for your feedback!": "",
-	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
+	"Thanks for your feedback!": "Grazie per il tuo feedback!",
+	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Il punteggio dovrebbe essere un valore compreso tra 0.0 (0%) e 1.0 (100%).",
 	"Theme": "Tema",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ciò garantisce che le tue preziose conversazioni siano salvate in modo sicuro nel tuo database backend. Grazie!",
 	"This setting does not sync across browsers or devices.": "Questa impostazione non si sincronizza tra browser o dispositivi.",
-	"Thorough explanation": "",
+	"Thorough explanation": "Spiegazione dettagliata",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Suggerimento: aggiorna più slot di variabili consecutivamente premendo il tasto tab nell'input della chat dopo ogni sostituzione.",
 	"Title": "Titolo",
-	"Title (e.g. Tell me a fun fact)": "",
+	"Title (e.g. Tell me a fun fact)": "Titolo (ad esempio Dimmi un fatto divertente)",
 	"Title Auto-Generation": "Generazione automatica del titolo",
-	"Title cannot be an empty string.": "",
+	"Title cannot be an empty string.": "Il titolo non può essere una stringa vuota.",
 	"Title Generation Prompt": "Prompt di generazione del titolo",
 	"to": "a",
 	"To access the available model names for downloading,": "Per accedere ai nomi dei modelli disponibili per il download,",
 	"To access the GGUF models available for downloading,": "Per accedere ai modelli GGUF disponibili per il download,",
 	"to chat input.": "all'input della chat.",
-	"Today": "",
+	"Today": "Oggi",
 	"Toggle settings": "Attiva/disattiva impostazioni",
 	"Toggle sidebar": "Attiva/disattiva barra laterale",
 	"Top K": "Top K",
@@ -454,7 +454,7 @@
 	"Type Hugging Face Resolve (Download) URL": "Digita l'URL di Hugging Face Resolve (Download)",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Si è verificato un problema durante la connessione a {{provider}}.",
 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo di file sconosciuto '{{file_type}}', ma accettato e trattato come testo normale",
-	"Update and Copy Link": "",
+	"Update and Copy Link": "Aggiorna e copia link",
 	"Update password": "Aggiorna password",
 	"Upload a GGUF model": "Carica un modello GGUF",
 	"Upload files": "Carica file",
@@ -462,7 +462,7 @@
 	"URL Mode": "Modalità URL",
 	"Use '#' in the prompt input to load and select your documents.": "Usa '#' nell'input del prompt per caricare e selezionare i tuoi documenti.",
 	"Use Gravatar": "Usa Gravatar",
-	"Use Initials": "",
+	"Use Initials": "Usa iniziali",
 	"user": "utente",
 	"User Permissions": "Autorizzazioni utente",
 	"Users": "Utenti",
@@ -471,11 +471,11 @@
 	"variable": "variabile",
 	"variable to have them replaced with clipboard content.": "variabile per farli sostituire con il contenuto degli appunti.",
 	"Version": "Versione",
-	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
+	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Attenzione: se aggiorni o cambi il tuo modello di embedding, dovrai reimportare tutti i documenti.",
 	"Web": "Web",
-	"Web Loader Settings": "",
-	"Web Params": "",
-	"Webhook URL": "",
+	"Web Loader Settings": "Impostazioni del caricatore Web",
+	"Web Params": "Parametri Web",
+	"Webhook URL": "URL webhook",
 	"WebUI Add-ons": "Componenti aggiuntivi WebUI",
 	"WebUI Settings": "Impostazioni WebUI",
 	"WebUI will make requests to": "WebUI effettuerà richieste a",
@@ -484,12 +484,12 @@
 	"Whisper (Local)": "Whisper (locale)",
 	"Write a prompt suggestion (e.g. Who are you?)": "Scrivi un suggerimento per il prompt (ad esempio Chi sei?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Scrivi un riassunto in 50 parole che riassume [argomento o parola chiave].",
-	"Yesterday": "",
+	"Yesterday": "Ieri",
 	"You": "Tu",
-	"You have no archived conversations.": "",
-	"You have shared this chat": "",
+	"You have no archived conversations.": "Non hai conversazioni archiviate.",
+	"You have shared this chat": "Hai condiviso questa chat",
 	"You're a helpful assistant.": "Sei un assistente utile.",
 	"You're now logged in.": "Ora hai effettuato l'accesso.",
-	"Youtube": "",
-	"Youtube Loader Settings": ""
+	"Youtube": "Youtube",
+	"Youtube Loader Settings": "Impostazioni del caricatore Youtube"
 }

+ 2 - 0
src/lib/stores/index.ts

@@ -9,6 +9,8 @@ export const user: Writable<SessionUser | undefined> = writable(undefined);
 // Frontend
 export const MODEL_DOWNLOAD_POOL = writable({});
 
+export const mobile = writable(false);
+
 export const theme = writable('system');
 export const chatId = writable('');
 

+ 66 - 0
src/lib/utils/index.test.ts

@@ -0,0 +1,66 @@
+import { promptTemplate } from '$lib/utils/index';
+import { expect, test } from 'vitest';
+
+test('promptTemplate correctly replaces {{prompt}} placeholder', () => {
+	const template = 'Hello {{prompt}}!';
+	const prompt = 'world';
+	const expected = 'Hello world!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate correctly replaces {{prompt:start:<length>}} placeholder', () => {
+	const template = 'Hello {{prompt:start:3}}!';
+	const prompt = 'world';
+	const expected = 'Hello wor!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate correctly replaces {{prompt:end:<length>}} placeholder', () => {
+	const template = 'Hello {{prompt:end:3}}!';
+	const prompt = 'world';
+	const expected = 'Hello rld!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate correctly replaces {{prompt:middletruncate:<length>}} placeholder when prompt length is greater than length', () => {
+	const template = 'Hello {{prompt:middletruncate:4}}!';
+	const prompt = 'world';
+	const expected = 'Hello wo...ld!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate correctly replaces {{prompt:middletruncate:<length>}} placeholder when prompt length is less than or equal to length', () => {
+	const template = 'Hello {{prompt:middletruncate:5}}!';
+	const prompt = 'world';
+	const expected = 'Hello world!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate returns original template when no placeholders are present', () => {
+	const template = 'Hello world!';
+	const prompt = 'world';
+	const expected = 'Hello world!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate does not replace placeholders inside of replaced placeholders', () => {
+	const template = 'Hello {{prompt}}!';
+	const prompt = 'World, {{prompt}} injection';
+	const expected = 'Hello World, {{prompt}} injection!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});
+
+test('promptTemplate correctly replaces multiple placeholders', () => {
+	const template = 'Hello {{prompt}}! This is {{prompt:start:3}}!';
+	const prompt = 'world';
+	const expected = 'Hello world! This is wor!';
+	const actual = promptTemplate(template, prompt);
+	expect(actual).toBe(expected);
+});

+ 32 - 15
src/lib/utils/index.ts

@@ -472,22 +472,39 @@ export const blobToFile = (blob, fileName) => {
 	return file;
 };
 
-export const promptTemplate = (template: string, prompt: string) => {
-	prompt = prompt.replace(/{{prompt}}|{{prompt:start:\d+}}|{{prompt:end:\d+}}/g, '');
-
-	template = template.replace(/{{prompt}}/g, prompt);
-
-	// Replace all instances of {{prompt:start:<length>}} with the first <length> characters of the prompt
-	template = template.replace(/{{prompt:start:(\d+)}}/g, (match, length) =>
-		prompt.substring(0, parseInt(length))
-	);
-
-	// Replace all instances of {{prompt:end:<length>}} with the last <length> characters of the prompt
-	template = template.replace(/{{prompt:end:(\d+)}}/g, (match, length) =>
-		prompt.slice(-parseInt(length))
+/**
+ * This function is used to replace placeholders in a template string with the provided prompt.
+ * The placeholders can be in the following formats:
+ * - `{{prompt}}`: This will be replaced with the entire prompt.
+ * - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt.
+ * - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt.
+ * - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle.
+ *
+ * @param {string} template - The template string containing placeholders.
+ * @param {string} prompt - The string to replace the placeholders with.
+ * @returns {string} The template string with the placeholders replaced by the prompt.
+ */
+export const promptTemplate = (template: string, prompt: string): string => {
+	return template.replace(
+		/{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g,
+		(match, startLength, endLength, middleLength) => {
+			if (match === '{{prompt}}') {
+				return prompt;
+			} else if (match.startsWith('{{prompt:start:')) {
+				return prompt.substring(0, startLength);
+			} else if (match.startsWith('{{prompt:end:')) {
+				return prompt.slice(-endLength);
+			} else if (match.startsWith('{{prompt:middletruncate:')) {
+				if (prompt.length <= middleLength) {
+					return prompt;
+				}
+				const start = prompt.slice(0, Math.ceil(middleLength / 2));
+				const end = prompt.slice(-Math.floor(middleLength / 2));
+				return `${start}...${end}`;
+			}
+			return '';
+		}
 	);
-
-	return template;
 };
 
 export const approximateToHumanReadable = (nanoseconds: number) => {

+ 18 - 1
src/routes/+layout.svelte

@@ -1,6 +1,6 @@
 <script>
 	import { onMount, tick, setContext } from 'svelte';
-	import { config, user, theme, WEBUI_NAME } from '$lib/stores';
+	import { config, user, theme, WEBUI_NAME, mobile } from '$lib/stores';
 	import { goto } from '$app/navigation';
 	import { Toaster, toast } from 'svelte-sonner';
 
@@ -18,9 +18,22 @@
 	setContext('i18n', i18n);
 
 	let loaded = false;
+	const BREAKPOINT = 1024;
 
 	onMount(async () => {
 		theme.set(localStorage.theme);
+
+		mobile.set(window.innerWidth < BREAKPOINT);
+		const onResize = () => {
+			if (window.innerWidth < BREAKPOINT) {
+				mobile.set(true);
+			} else {
+				mobile.set(false);
+			}
+		};
+
+		window.addEventListener('resize', onResize);
+
 		let backendConfig = null;
 		try {
 			backendConfig = await getBackendConfig();
@@ -67,6 +80,10 @@
 
 		document.getElementById('splash-screen')?.remove();
 		loaded = true;
+
+		return () => {
+			window.removeEventListener('resize', onResize);
+		};
 	});
 </script>
 

Some files were not shown because too many files changed in this diff