Unverified Commit db0712ae authored by Danny Liu's avatar Danny Liu Committed by GitHub
Browse files

Merge branch 'dev' into feat/system-wide-theme

parents f1716f45 e414b9ea
...@@ -77,3 +77,65 @@ export const getVersionUpdates = async () => { ...@@ -77,3 +77,65 @@ export const getVersionUpdates = async () => {
return res; return res;
}; };
export const getModelFilterConfig = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateModelFilterConfig = async (
token: string,
enabled: boolean,
models: string[]
) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
enabled: enabled,
models: models
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
import { RAG_API_BASE_URL } from '$lib/constants'; import { RAG_API_BASE_URL } from '$lib/constants';
export const getChunkParams = async (token: string) => { export const getRAGConfig = async (token: string) => {
let error = null; let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/chunk`, { const res = await fetch(`${RAG_API_BASE_URL}/config`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => { ...@@ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => {
return res; return res;
}; };
export const updateChunkParams = async (token: string, size: number, overlap: number) => { type ChunkConfigForm = {
chunk_size: number;
chunk_overlap: number;
};
type RAGConfigForm = {
pdf_extract_images: boolean;
chunk: ChunkConfigForm;
};
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
let error = null; let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/chunk/update`, { const res = await fetch(`${RAG_API_BASE_URL}/config/update`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
chunk_size: size, ...payload
chunk_overlap: overlap
}) })
}) })
.then(async (res) => { .then(async (res) => {
......
<script>
import { getContext } from 'svelte';
const i18n = getContext('i18n');
</script>
<div class=" text-center text-6xl mb-3">📄</div> <div class=" text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<slot <slot
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> ><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to the conversation {$i18n.t('Drop any files here to add to the conversation')}
</div> </div>
</slot> </slot>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { Confetti } from 'svelte-confetti'; import { Confetti } from 'svelte-confetti';
import { WEBUI_NAME, config } from '$lib/stores'; import { WEBUI_NAME, config } from '$lib/stores';
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
import Modal from './common/Modal.svelte'; import Modal from './common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
let changelog = null; let changelog = null;
...@@ -23,7 +25,8 @@ ...@@ -23,7 +25,8 @@
<div class="px-5 py-4 dark:text-gray-300"> <div class="px-5 py-4 dark:text-gray-300">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="text-xl font-bold"> <div class="text-xl font-bold">
What’s New in {$WEBUI_NAME} {$i18n.t('What’s New in')}
{$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} /> <Confetti x={[-1, -0.25]} y={[0, 0.5]} />
</div> </div>
<button <button
...@@ -45,7 +48,7 @@ ...@@ -45,7 +48,7 @@
</button> </button>
</div> </div>
<div class="flex items-center mt-1"> <div class="flex items-center mt-1">
<div class="text-sm dark:text-gray-200">Release Notes</div> <div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<div class="text-sm dark:text-gray-200"> <div class="text-sm dark:text-gray-200">
v{WEBUI_VERSION} v{WEBUI_VERSION}
...@@ -108,7 +111,7 @@ ...@@ -108,7 +111,7 @@
}} }}
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
> >
<span class="relative">Okay, Let's Go!</span> <span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
</button> </button>
</div> </div>
</div> </div>
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { updateUserById } from '$lib/apis/users'; import { updateUserById } from '$lib/apis/users';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let show = false; export let show = false;
...@@ -42,7 +43,7 @@ ...@@ -42,7 +43,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Edit User</div> <div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
...@@ -84,7 +85,8 @@ ...@@ -84,7 +85,8 @@
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div> <div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')} {$i18n.t('Created at')}
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
</div> </div>
</div> </div>
</div> </div>
...@@ -93,7 +95,7 @@ ...@@ -93,7 +95,7 @@
<div class=" flex flex-col space-y-1.5"> <div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Email</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
...@@ -108,7 +110,7 @@ ...@@ -108,7 +110,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
...@@ -122,7 +124,7 @@ ...@@ -122,7 +124,7 @@
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
...@@ -140,7 +142,7 @@ ...@@ -140,7 +142,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>
......
<script lang="ts"> <script lang="ts">
import { downloadDatabase } from '$lib/apis/utils'; import { downloadDatabase } from '$lib/apis/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
...@@ -17,10 +19,10 @@ ...@@ -17,10 +19,10 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">Database</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> --> <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
<button <button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
...@@ -46,7 +48,7 @@ ...@@ -46,7 +48,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">Download Database</div> <div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button> </button>
</div> </div>
</div> </div>
......
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
updateDefaultUserRole, updateDefaultUserRole,
updateJWTExpiresDuration updateJWTExpiresDuration
} from '$lib/apis/auths'; } from '$lib/apis/auths';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let signUpEnabled = true; let signUpEnabled = true;
...@@ -43,10 +45,10 @@ ...@@ -43,10 +45,10 @@
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">General Settings</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Enable New Sign Ups</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -66,7 +68,7 @@ ...@@ -66,7 +68,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/> />
</svg> </svg>
<span class="ml-2 self-center">Enabled</span> <span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -81,25 +83,25 @@ ...@@ -81,25 +83,25 @@
/> />
</svg> </svg>
<span class="ml-2 self-center">Disabled</span> <span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
{/if} {/if}
</button> </button>
</div> </div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Default User Role</div> <div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={defaultUserRole} bind:value={defaultUserRole}
placeholder="Select a theme" placeholder="Select a theme"
on:change={(e) => { on:change={(e) => {
updateDefaultUserRoleHandler(e.target.value); updateDefaultUserRoleHandler(e.target.value);
}} }}
> >
<option value="pending">Pending</option> <option value="pending">{$i18n.t('pending')}</option>
<option value="user">User</option> <option value="user">{$i18n.t('user')}</option>
<option value="admin">Admin</option> <option value="admin">{$i18n.t('admin')}</option>
</select> </select>
</div> </div>
</div> </div>
...@@ -108,7 +110,7 @@ ...@@ -108,7 +110,7 @@
<div class=" w-full justify-between"> <div class=" w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Expiration</div> <div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
</div> </div>
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
...@@ -121,8 +123,9 @@ ...@@ -121,8 +123,9 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Valid time units: <span class=" text-gray-300 font-medium" {$i18n.t('Valid time units:')}
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span <span class=" text-gray-300 font-medium"
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
> >
</div> </div>
</div> </div>
...@@ -134,7 +137,7 @@ ...@@ -134,7 +137,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>
<script lang="ts"> <script lang="ts">
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let whitelistEnabled = false;
let whitelistModels = [''];
let permissions = { let permissions = {
chat: { chat: {
deletion: true deletion: true
...@@ -13,6 +20,13 @@ ...@@ -13,6 +20,13 @@
onMount(async () => { onMount(async () => {
permissions = await getUserPermissions(localStorage.token); permissions = await getUserPermissions(localStorage.token);
const res = await getModelFilterConfig(localStorage.token);
if (res) {
whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : [''];
}
}); });
</script> </script>
...@@ -21,15 +35,17 @@ ...@@ -21,15 +35,17 @@
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
// console.log('submit'); // console.log('submit');
await updateUserPermissions(localStorage.token, permissions); await updateUserPermissions(localStorage.token, permissions);
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div> <div>
<div class=" mb-2 text-sm font-medium">User Permissions</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Allow Chat Deletion</div> <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -49,7 +65,7 @@ ...@@ -49,7 +65,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/> />
</svg> </svg>
<span class="ml-2 self-center">Allow</span> <span class="ml-2 self-center">{$i18n.t('Allow')}</span>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -64,11 +80,112 @@ ...@@ -64,11 +80,112 @@
/> />
</svg> </svg>
<span class="ml-2 self-center">Don't Allow</span> <span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
{/if} {/if}
</button> </button>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" />
<div class="mt-2 space-y-3 pr-1.5">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
</div>
</div>
<div class=" space-y-3">
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
whitelistEnabled = !whitelistEnabled;
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
{#if whitelistEnabled}
<div>
<div class=" space-y-1.5">
{#each whitelistModels as modelId, modelIdx}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={modelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{model.name}</option
>
{/each}
</select>
</div>
{#if modelIdx === 0}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
if (whitelistModels.at(-1) !== '') {
whitelistModels = [...whitelistModels, ''];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
{:else}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
whitelistModels.splice(modelIdx, 1);
whitelistModels = whitelistModels;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
<div class=" text-xs font-medium">
{whitelistModels.length}
{$i18n.t('Model(s) Whitelisted')}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
...@@ -76,7 +193,7 @@ ...@@ -76,7 +193,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>
<script> <script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte'; import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte'; import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte'; import Users from './Settings/Users.svelte';
const i18n = getContext('i18n');
export let show = false; export let show = false;
let selectedTab = 'general'; let selectedTab = 'general';
...@@ -13,7 +16,7 @@ ...@@ -13,7 +16,7 @@
<Modal bind:show> <Modal bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Admin Settings</div> <div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
...@@ -61,7 +64,7 @@ ...@@ -61,7 +64,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">General</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </button>
<button <button
...@@ -85,7 +88,7 @@ ...@@ -85,7 +88,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Users</div> <div class=" self-center">{$i18n.t('Users')}</div>
</button> </button>
<button <button
...@@ -113,7 +116,7 @@ ...@@ -113,7 +116,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">Database</div> <div class=" self-center">{$i18n.t('Database')}</div>
</button> </button>
</div> </div>
<div class="flex-1 md:min-h-[380px]"> <div class="flex-1 md:min-h-[380px]">
......
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
...@@ -14,12 +14,14 @@ ...@@ -14,12 +14,14 @@
import { transcribeAudio } from '$lib/apis/audio'; import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function; export let submitPrompt: Function;
export let stopResponse: Function; export let stopResponse: Function;
export let suggestionPrompts = []; export let suggestionPrompts = [];
export let autoScroll = true; export let autoScroll = true;
let chatTextAreaElement:HTMLTextAreaElement let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement; let filesInputElement;
let promptsElement; let promptsElement;
...@@ -209,11 +211,11 @@ ...@@ -209,11 +211,11 @@
// Event triggered when an error occurs // Event triggered when an error occurs
speechRecognition.onerror = function (event) { speechRecognition.onerror = function (event) {
console.log(event); console.log(event);
toast.error(`Speech recognition error: ${event.error}`); toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
isRecording = false; isRecording = false;
}; };
} else { } else {
toast.error('SpeechRecognition API is not supported in this browser.'); toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
} }
} }
} }
...@@ -333,12 +335,15 @@ ...@@ -333,12 +335,15 @@
uploadDoc(file); uploadDoc(file);
} else { } else {
toast.error( toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text` $i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
); );
uploadDoc(file); uploadDoc(file);
} }
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
} }
...@@ -359,12 +364,12 @@ ...@@ -359,12 +364,12 @@
{#if dragged} {#if dragged}
<div <div
class="fixed w-full h-full flex z-50 touch-none pointer-events-none" class="fixed lg:w-[calc(100%-260px)] w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone" id="dropzone"
role="region" role="region"
aria-label="Drag and Drop Container" aria-label="Drag and Drop Container"
> >
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center"> <div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<AddFilesPlaceholder /> <AddFilesPlaceholder />
...@@ -477,13 +482,16 @@ ...@@ -477,13 +482,16 @@
filesInputElement.value = ''; filesInputElement.value = '';
} else { } else {
toast.error( toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text` $i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
); );
uploadDoc(file); uploadDoc(file);
filesInputElement.value = ''; filesInputElement.value = '';
} }
} else { } else {
toast.error(`File not found.`); toast.error($i18n.t(`File not found.`));
} }
}} }}
/> />
...@@ -570,7 +578,7 @@ ...@@ -570,7 +578,7 @@
{file.name} {file.name}
</div> </div>
<div class=" text-gray-500 text-sm">Document</div> <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div> </div>
</div> </div>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
...@@ -598,7 +606,7 @@ ...@@ -598,7 +606,7 @@
{file?.title ?? `#${file.name}`} {file?.title ?? `#${file.name}`}
</div> </div>
<div class=" text-gray-500 text-sm">Collection</div> <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div> </div>
</div> </div>
{/if} {/if}
...@@ -632,7 +640,7 @@ ...@@ -632,7 +640,7 @@
<div class=" flex"> <div class=" flex">
{#if fileUploadEnabled} {#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1"> <div class=" self-end mb-2 ml-1">
<Tooltip content="Upload files"> <Tooltip content={$i18n.t('Upload files')}>
<button <button
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5" class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button" type="button"
...@@ -664,8 +672,8 @@ ...@@ -664,8 +672,8 @@
placeholder={chatInputPlaceholder !== '' placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder ? chatInputPlaceholder
: isRecording : isRecording
? 'Listening...' ? $i18n.t('Listening...')
: 'Send a message'} : $i18n.t('Send a Messsage')}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
if (e.keyCode == 13 && !e.shiftKey) { if (e.keyCode == 13 && !e.shiftKey) {
...@@ -804,7 +812,7 @@ ...@@ -804,7 +812,7 @@
<div class="self-end mb-2 flex space-x-1 mr-1"> <div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true} {#if messages.length == 0 || messages.at(-1).done == true}
<Tooltip content="Record voice"> <Tooltip content={$i18n.t('Record voice')}>
{#if speechRecognitionEnabled} {#if speechRecognitionEnabled}
<button <button
id="voice-input-button" id="voice-input-button"
...@@ -873,7 +881,7 @@ ...@@ -873,7 +881,7 @@
{/if} {/if}
</Tooltip> </Tooltip>
<Tooltip content="Send message"> <Tooltip content={$i18n.t('Send message')}>
<button <button
class="{prompt !== '' class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
...@@ -919,7 +927,7 @@ ...@@ -919,7 +927,7 @@
</form> </form>
<div class="mt-1.5 text-xs text-gray-500 text-center"> <div class="mt-1.5 text-xs text-gray-500 text-center">
LLMs can make mistakes. Verify important information. {$i18n.t('LLMs can make mistakes. Verify important information.')}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
import { documents } from '$lib/stores'; import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils'; import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -117,7 +119,7 @@ ...@@ -117,7 +119,7 @@
{doc?.title ?? `#${doc.name}`} {doc?.title ?? `#${doc.name}`}
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div> <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
{:else} {:else}
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black line-clamp-1">
#{doc.name} ({doc.filename}) #{doc.name} ({doc.filename})
...@@ -140,7 +142,9 @@ ...@@ -140,7 +142,9 @@
confirmSelectWeb(url); confirmSelectWeb(url);
} else { } else {
toast.error( toast.error(
'Oops! Looks like the URL is invalid. Please double-check and try again.' $i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
); );
} }
}} }}
...@@ -149,7 +153,7 @@ ...@@ -149,7 +153,7 @@
{prompt.split(' ')?.at(0)?.substring(1)} {prompt.split(' ')?.at(0)?.substring(1)}
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1">Web</div> <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
</button> </button>
{/if} {/if}
</div> </div>
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
import { generatePrompt } from '$lib/apis/ollama'; import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores'; import { models } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
export let user = null; export let user = null;
...@@ -41,7 +43,7 @@ ...@@ -41,7 +43,7 @@
user = JSON.parse(JSON.stringify(model.name)); user = JSON.parse(JSON.stringify(model.name));
await tick(); await tick();
chatInputPlaceholder = `'${model.name}' is thinking...`; chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
const chatInputElement = document.getElementById('chat-textarea'); const chatInputElement = document.getElementById('chat-textarea');
...@@ -113,7 +115,9 @@ ...@@ -113,7 +115,9 @@
toast.error(error.error); toast.error(error.error);
} }
} else { } else {
toast.error(`Uh-oh! There was an issue connecting to Ollama.`); toast.error(
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
);
} }
} }
......
<script lang="ts"> <script lang="ts">
import { prompts } from '$lib/stores'; import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils'; import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = ''; export let prompt = '';
let selectedCommandIdx = 0; let selectedCommandIdx = 0;
let filteredPromptCommands = []; let filteredPromptCommands = [];
...@@ -29,7 +31,7 @@ ...@@ -29,7 +31,7 @@
if (command.content.includes('{{CLIPBOARD}}')) { if (command.content.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => { const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error('Failed to read clipboard contents'); toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}'; return '{{CLIPBOARD}}';
}); });
...@@ -113,8 +115,9 @@ ...@@ -113,8 +115,9 @@
</div> </div>
<div class="line-clamp-1"> <div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat {$i18n.t(
input after each replacement. 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores'; import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte'; import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats'; import { getChatList, updateChatById } from '$lib/apis/chats';
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
const i18n = getContext('i18n');
export let chatId = ''; export let chatId = '';
export let sendPrompt: Function; export let sendPrompt: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
navigator.clipboard.writeText(text).then( navigator.clipboard.writeText(text).then(
function () { function () {
console.log('Async: Copying to clipboard was successful!'); console.log('Async: Copying to clipboard was successful!');
toast.success('Copying to clipboard was successful!'); toast.success($i18n.t('Copying to clipboard was successful!'));
}, },
function (err) { function (err) {
console.error('Async: Could not copy text: ', err); console.error('Async: Could not copy text: ', err);
......
<script lang="ts"> <script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let models = []; export let models = [];
export let modelfiles = []; export let modelfiles = [];
...@@ -64,9 +66,9 @@ ...@@ -64,9 +66,9 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div class=" line-clamp-1">Hello, {$user.name}</div> <div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
<div>How can I help you today?</div> <div>{$i18n.t('How can I help you today?')}</div>
{/if} {/if}
</div> </div>
</div> </div>
......
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -316,7 +318,7 @@ ...@@ -316,7 +318,7 @@
{#if message.timestamp} {#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> <span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} {dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
</span> </span>
{/if} {/if}
</Name> </Name>
...@@ -360,7 +362,7 @@ ...@@ -360,7 +362,7 @@
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
<button <button
...@@ -369,7 +371,7 @@ ...@@ -369,7 +371,7 @@
cancelEditMessage(); cancelEditMessage();
}} }}
> >
Cancel {$i18n.t('Cancel')}
</button> </button>
</div> </div>
</div> </div>
......
<script lang="ts"> <script lang="ts">
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tick, createEventDispatcher } from 'svelte'; import { tick, createEventDispatcher, getContext } from 'svelte';
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores'; import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let user; export let user;
...@@ -65,17 +67,18 @@ ...@@ -65,17 +67,18 @@
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)} {#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title} {$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{:else} {:else}
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span> {$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if} {/if}
{:else if $settings.showUsername} {:else if $settings.showUsername}
{user.name} {user.name}
{:else} {:else}
You {$i18n.t('You')}
{/if} {/if}
{#if message.timestamp} {#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> <span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} {dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
</span> </span>
{/if} {/if}
</Name> </Name>
...@@ -123,7 +126,7 @@ ...@@ -123,7 +126,7 @@
{file.name} {file.name}
</div> </div>
<div class=" text-gray-500 text-sm">Document</div> <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div> </div>
</button> </button>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
...@@ -152,7 +155,7 @@ ...@@ -152,7 +155,7 @@
{file?.title ?? `#${file.name}`} {file?.title ?? `#${file.name}`}
</div> </div>
<div class=" text-gray-500 text-sm">Collection</div> <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div> </div>
</button> </button>
{/if} {/if}
...@@ -181,7 +184,7 @@ ...@@ -181,7 +184,7 @@
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
> >
Save & Submit {$i18n.t('Save & Submit')}
</button> </button>
<button <button
...@@ -190,7 +193,7 @@ ...@@ -190,7 +193,7 @@
cancelEditMessage(); cancelEditMessage();
}} }}
> >
Cancel {$i18n.t('Cancel')}
</button> </button>
</div> </div>
</div> </div>
......
<script lang="ts"> <script lang="ts">
import { setDefaultModels } from '$lib/apis/configs'; import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores'; import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let selectedModels = ['']; export let selectedModels = [''];
export let disabled = false; export let disabled = false;
const saveDefaultModel = async () => { const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === ''); const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) { if (hasEmptyModel.length) {
toast.error('Choose a model before saving...'); toast.error($i18n.t('Choose a model before saving...'));
return; return;
} }
settings.set({ ...$settings, models: selectedModels }); settings.set({ ...$settings, models: selectedModels });
...@@ -20,7 +22,7 @@ ...@@ -20,7 +22,7 @@
console.log('setting default models globally'); console.log('setting default models globally');
await setDefaultModels(localStorage.token, selectedModels.join(',')); await setDefaultModels(localStorage.token, selectedModels.join(','));
} }
toast.success('Default model updated'); toast.success($i18n.t('Default model updated'));
}; };
$: if (selectedModels.length > 0 && $models.length > 0) { $: if (selectedModels.length > 0 && $models.length > 0) {
...@@ -39,7 +41,9 @@ ...@@ -39,7 +41,9 @@
bind:value={selectedModel} bind:value={selectedModel}
{disabled} {disabled}
> >
<option class=" text-gray-700" value="" selected disabled>Select a model</option> <option class=" text-gray-700" value="" selected disabled
>{$i18n.t('Select a model')}</option
>
{#each $models as model} {#each $models as model}
{#if model.name === 'hr'} {#if model.name === 'hr'}
...@@ -133,5 +137,5 @@ ...@@ -133,5 +137,5 @@
</div> </div>
<div class="text-left mt-1.5 text-xs text-gray-500"> <div class="text-left mt-1.5 text-xs text-gray-500">
<button on:click={saveDefaultModel}> Set as default</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import { WEBUI_VERSION } from '$lib/constants'; import { WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores'; import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let ollamaVersion = ''; let ollamaVersion = '';
...@@ -43,7 +45,8 @@ ...@@ -43,7 +45,8 @@
<div> <div>
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center"> <div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
<div> <div>
{$WEBUI_NAME} Version {$WEBUI_NAME}
{$i18n.t('Version')}
</div> </div>
</div> </div>
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
...@@ -56,10 +59,10 @@ ...@@ -56,10 +59,10 @@
target="_blank" target="_blank"
> >
{updateAvailable === null {updateAvailable === null
? 'Checking for updates...' ? $i18n.t('Checking for updates...')
: updateAvailable : updateAvailable
? `(v${version.latest} available!)` ? `(v${version.latest} ${$i18n.t('available!')})`
: '(latest)'} : $i18n.t('(latest)')}
</a> </a>
</div> </div>
...@@ -69,7 +72,7 @@ ...@@ -69,7 +72,7 @@
showChangelog.set(true); showChangelog.set(true);
}} }}
> >
<div>See what's new</div> <div>{$i18n.t("See what's new")}</div>
</button> </button>
</div> </div>
...@@ -79,7 +82,7 @@ ...@@ -79,7 +82,7 @@
checkForVersionUpdates(); checkForVersionUpdates();
}} }}
> >
Check for updates {$i18n.t('Check for updates')}
</button> </button>
</div> </div>
</div> </div>
...@@ -88,7 +91,7 @@ ...@@ -88,7 +91,7 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'} {ollamaVersion ?? 'N/A'}
...@@ -123,7 +126,8 @@ ...@@ -123,7 +126,8 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Created by <a {$i18n.t('Created by')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck" href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a target="_blank">Timothy J. Baek</a
......
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount } from 'svelte'; import { onMount, getContext } from 'svelte';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths'; import { updateUserProfile } from '$lib/apis/auths';
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let profileImageUrl = ''; let profileImageUrl = '';
...@@ -38,7 +40,7 @@ ...@@ -38,7 +40,7 @@
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
<input <input
id="profile-image-input" id="profile-image-input"
bind:this={profileImageInputElement} bind:this={profileImageInputElement}
...@@ -101,7 +103,7 @@ ...@@ -101,7 +103,7 @@
}} }}
/> />
<div class=" mb-2.5 text-sm font-medium">Profile</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
<div class="flex space-x-5"> <div class="flex space-x-5">
<div class="flex flex-col"> <div class="flex flex-col">
...@@ -143,13 +145,13 @@ ...@@ -143,13 +145,13 @@
const url = await getGravatarUrl($user.email); const url = await getGravatarUrl($user.email);
profileImageUrl = url; profileImageUrl = url;
}}>Use Gravatar</button }}>{$i18n.t('Use Gravatar')}</button
> >
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
...@@ -170,7 +172,7 @@ ...@@ -170,7 +172,7 @@
<div class=" w-full justify-between"> <div class=" w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Token</div> <div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div> </div>
<div class="flex mt-2"> <div class="flex mt-2">
...@@ -271,7 +273,7 @@ ...@@ -271,7 +273,7 @@
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={async () => { on:click={async () => {
const res = await submitHandler(); const res = await submitHandler();
...@@ -280,7 +282,7 @@ ...@@ -280,7 +282,7 @@
} }
}} }}
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</div> </div>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment