Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
621719c6
Unverified
Commit
621719c6
authored
Mar 18, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Mar 18, 2024
Browse files
Merge pull request #1187 from open-webui/dev
0.1.113
parents
5ce421e7
ec9895a1
Changes
82
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
364 additions
and
271 deletions
+364
-271
src/lib/components/admin/SettingsModal.svelte
src/lib/components/admin/SettingsModal.svelte
+7
-4
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+23
-15
src/lib/components/chat/MessageInput/Documents.svelte
src/lib/components/chat/MessageInput/Documents.svelte
+8
-4
src/lib/components/chat/MessageInput/Models.svelte
src/lib/components/chat/MessageInput/Models.svelte
+7
-3
src/lib/components/chat/MessageInput/PromptCommands.svelte
src/lib/components/chat/MessageInput/PromptCommands.svelte
+7
-4
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+4
-2
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+5
-3
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+6
-4
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+11
-8
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+9
-5
src/lib/components/chat/Settings/About.svelte
src/lib/components/chat/Settings/About.svelte
+13
-9
src/lib/components/chat/Settings/Account.svelte
src/lib/components/chat/Settings/Account.svelte
+9
-7
src/lib/components/chat/Settings/Account/UpdatePassword.svelte
...ib/components/chat/Settings/Account/UpdatePassword.svelte
+10
-7
src/lib/components/chat/Settings/Advanced.svelte
src/lib/components/chat/Settings/Advanced.svelte
+13
-11
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
...b/components/chat/Settings/Advanced/AdvancedParams.svelte
+40
-36
src/lib/components/chat/Settings/Audio.svelte
src/lib/components/chat/Settings/Audio.svelte
+33
-25
src/lib/components/chat/Settings/Chats.svelte
src/lib/components/chat/Settings/Chats.svelte
+17
-13
src/lib/components/chat/Settings/Connections.svelte
src/lib/components/chat/Settings/Connections.svelte
+16
-13
src/lib/components/chat/Settings/General.svelte
src/lib/components/chat/Settings/General.svelte
+95
-71
src/lib/components/chat/Settings/Images.svelte
src/lib/components/chat/Settings/Images.svelte
+31
-27
No files found.
src/lib/components/admin/SettingsModal.svelte
View file @
621719c6
<script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte';
const i18n = getContext('i18n');
export let show = false;
let selectedTab = 'general';
...
...
@@ -13,7 +16,7 @@
<Modal bind:show>
<div>
<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
class="self-center"
on:click={() => {
...
...
@@ -61,7 +64,7 @@
/>
</svg>
</div>
<div class=" self-center">General</div>
<div class=" self-center">
{$i18n.t('
General
')}
</div>
</button>
<button
...
...
@@ -85,7 +88,7 @@
/>
</svg>
</div>
<div class=" self-center">Users</div>
<div class=" self-center">
{$i18n.t('
Users
')}
</div>
</button>
<button
...
...
@@ -113,7 +116,7 @@
/>
</svg>
</div>
<div class=" self-center">Database</div>
<div class=" self-center">
{$i18n.t('
Database
')}
</div>
</button>
</div>
<div class="flex-1 md:min-h-[380px]">
...
...
src/lib/components/chat/MessageInput.svelte
View file @
621719c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte';
import { onMount, tick
, getContext
} from 'svelte';
import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
...
...
@@ -14,6 +14,8 @@
import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let stopResponse: Function;
...
...
@@ -209,11 +211,11 @@
// Event triggered when an error occurs
speechRecognition.onerror = function (event) {
console.log(event);
toast.error(`Speech recognition error:
$
{event.error
}`
);
toast.error(
$i18n.t(
`Speech recognition error:
{
{
error}}`, { error:
event.error
})
);
isRecording = false;
};
} 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 @@
uploadDoc(file);
} else {
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);
}
} else {
toast.error(`File not found.`);
toast.error(
$i18n.t(
`File not found.`)
)
;
}
}
...
...
@@ -477,13 +482,16 @@
filesInputElement.value = '';
} else {
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);
filesInputElement.value = '';
}
} else {
toast.error(`File not found.`);
toast.error(
$i18n.t(
`File not found.`)
)
;
}
}}
/>
...
...
@@ -570,7 +578,7 @@
{file.name}
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Document
')}
</div>
</div>
</div>
{:else if file.type === 'collection'}
...
...
@@ -598,7 +606,7 @@
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Collection
')}
</div>
</div>
</div>
{/if}
...
...
@@ -632,7 +640,7 @@
<div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1">
<Tooltip content=
"
Upload files
"
>
<Tooltip content=
{$i18n.t('
Upload files
')}
>
<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"
type="button"
...
...
@@ -664,8 +672,8 @@
placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder
: isRecording
? 'Listening...'
: 'Send a
me
ssage'}
?
$i18n.t(
'Listening...'
)
:
$i18n.t(
'Send a
Mes
ssage'
)
}
bind:value={prompt}
on:keypress={(e) => {
if (e.keyCode == 13 && !e.shiftKey) {
...
...
@@ -804,7 +812,7 @@
<div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true}
<Tooltip content=
"
Record voice
"
>
<Tooltip content=
{$i18n.t('
Record voice
')}
>
{#if speechRecognitionEnabled}
<button
id="voice-input-button"
...
...
@@ -873,7 +881,7 @@
{/if}
</Tooltip>
<Tooltip content=
"
Send message
"
>
<Tooltip content=
{$i18n.t('
Send message
')}
>
<button
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
...
...
@@ -919,7 +927,7 @@
</form>
<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>
...
...
src/lib/components/chat/MessageInput/Documents.svelte
View file @
621719c6
...
...
@@ -3,9 +3,11 @@
import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte';
import { tick
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
const dispatch = createEventDispatcher();
...
...
@@ -117,7 +119,7 @@
{doc?.title ?? `#${doc.name}`}
</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}
<div class=" font-medium text-black line-clamp-1">
#{doc.name} ({doc.filename})
...
...
@@ -140,7 +142,9 @@
confirmSelectWeb(url);
} else {
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 @@
{prompt.split(' ')?.at(0)?.substring(1)}
</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>
{/if}
</div>
...
...
src/lib/components/chat/MessageInput/Models.svelte
View file @
621719c6
...
...
@@ -2,9 +2,11 @@
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { tick } from 'svelte';
import { tick
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let user = null;
...
...
@@ -41,7 +43,7 @@
user = JSON.parse(JSON.stringify(model.name));
await tick();
chatInputPlaceholder =
`'$
{model
.n
ame}
'
is thinking...
`
;
chatInputPlaceholder =
$i18n.t('{
{model
N
ame}
}
is thinking...
', { modelName: model.name })
;
const chatInputElement = document.getElementById('chat-textarea');
...
...
@@ -113,7 +115,9 @@
toast.error(error.error);
}
} 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' })
);
}
}
...
...
src/lib/components/chat/MessageInput/PromptCommands.svelte
View file @
621719c6
<script lang="ts">
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte';
import { tick
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
let selectedCommandIdx = 0;
let filteredPromptCommands = [];
...
...
@@ -29,7 +31,7 @@
if (command.content.includes('{{CLIPBOARD}}')) {
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}}';
});
...
...
@@ -113,8 +115,9 @@
</div>
<div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
input after each replacement.
{$i18n.t(
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div>
</div>
</div>
...
...
src/lib/components/chat/Messages.svelte
View file @
621719c6
...
...
@@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte';
import { tick
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
...
...
@@ -13,6 +13,8 @@
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
const i18n = getContext('i18n');
export let chatId = '';
export let sendPrompt: Function;
export let continueGeneration: Function;
...
...
@@ -67,7 +69,7 @@
navigator.clipboard.writeText(text).then(
function () {
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) {
console.error('Async: Could not copy text: ', err);
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
621719c6
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let models = [];
export let modelfiles = [];
...
...
@@ -64,9 +66,9 @@
</div>
{/if}
{: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}
</div>
</div>
...
...
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
621719c6
...
...
@@ -8,7 +8,9 @@
import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { onMount, tick } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
...
...
@@ -316,7 +318,7 @@
{#if message.timestamp}
<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>
{/if}
</Name>
...
...
@@ -360,7 +362,7 @@
editMessageConfirmHandler();
}}
>
Save
{$i18n.t('
Save
')}
</button>
<button
...
...
@@ -369,7 +371,7 @@
cancelEditMessage();
}}
>
Cancel
{$i18n.t('
Cancel
')}
</button>
</div>
</div>
...
...
src/lib/components/chat/Messages/UserMessage.svelte
View file @
621719c6
<script lang="ts">
import dayjs from 'dayjs';
import { tick, createEventDispatcher } from 'svelte';
import { tick, createEventDispatcher
, getContext
} from 'svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let user;
...
...
@@ -65,17 +67,18 @@
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{: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}
{:else if $settings.showUsername}
{user.name}
{:else}
You
{$i18n.t('You')}
{/if}
{#if message.timestamp}
<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>
{/if}
</Name>
...
...
@@ -123,7 +126,7 @@
{file.name}
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Document
')}
</div>
</div>
</button>
{:else if file.type === 'collection'}
...
...
@@ -152,7 +155,7 @@
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Collection
')}
</div>
</div>
</button>
{/if}
...
...
@@ -181,7 +184,7 @@
editMessageConfirmHandler();
}}
>
Save & Submit
{$i18n.t('
Save & Submit
')}
</button>
<button
...
...
@@ -190,7 +193,7 @@
cancelEditMessage();
}}
>
Cancel
{$i18n.t('
Cancel
')}
</button>
</div>
</div>
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
621719c6
<script lang="ts">
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte';
import { onMount, tick
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let selectedModels = [''];
export let disabled = false;
const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) {
toast.error('Choose a model before saving...');
toast.error(
$i18n.t(
'Choose a model before saving...')
)
;
return;
}
settings.set({ ...$settings, models: selectedModels });
...
...
@@ -20,7 +22,7 @@
console.log('setting default models globally');
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) {
...
...
@@ -39,7 +41,9 @@
bind:value={selectedModel}
{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}
{#if model.name === 'hr'}
...
...
@@ -133,5 +137,5 @@
</div>
<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>
src/lib/components/chat/Settings/About.svelte
View file @
621719c6
...
...
@@ -4,7 +4,9 @@
import { WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let ollamaVersion = '';
...
...
@@ -43,7 +45,8 @@
<div>
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
<div>
{$WEBUI_NAME} Version
{$WEBUI_NAME}
{$i18n.t('Version')}
</div>
</div>
<div class="flex w-full justify-between items-center">
...
...
@@ -56,10 +59,10 @@
target="_blank"
>
{updateAvailable === null
? 'Checking for updates...'
?
$i18n.t(
'Checking for updates...'
)
: updateAvailable
? `(v${version.latest} available!)`
: '(latest)'}
? `(v${version.latest}
${$i18n.t('
available!
')}
)`
:
$i18n.t(
'(latest)'
)
}
</a>
</div>
...
...
@@ -69,7 +72,7 @@
showChangelog.set(true);
}}
>
<div>See what's new</div>
<div>
{$i18n.t("
See what's new
")}
</div>
</button>
</div>
...
...
@@ -79,7 +82,7 @@
checkForVersionUpdates();
}}
>
Check for updates
{$i18n.t('
Check for updates
')}
</button>
</div>
</div>
...
...
@@ -88,7 +91,7 @@
<hr class=" dark:border-gray-700" />
<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-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'}
...
...
@@ -123,7 +126,8 @@
</div>
<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"
href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a
...
...
src/lib/components/chat/Settings/Account.svelte
View file @
621719c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { onMount
, getContext
} from 'svelte';
import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths';
...
...
@@ -9,6 +9,8 @@
import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils';
const i18n = getContext('i18n');
export let saveHandler: Function;
let profileImageUrl = '';
...
...
@@ -38,7 +40,7 @@
</script>
<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
id="profile-image-input"
bind:this={profileImageInputElement}
...
...
@@ -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 flex-col">
...
...
@@ -143,13 +145,13 @@
const url = await getGravatarUrl($user.email);
profileImageUrl = url;
}}>Use Gravatar</button
}}>
{$i18n.t('
Use Gravatar
')}
</button
>
</div>
<div class="flex-1">
<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">
<input
...
...
@@ -170,7 +172,7 @@
<div class=" 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 class="flex mt-2">
...
...
@@ -280,7 +282,7 @@
}
}}
>
Save
{$i18n.t('
Save
')}
</button>
</div>
</div>
src/lib/components/chat/Settings/Account/UpdatePassword.svelte
View file @
621719c6
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { updateUserPassword } from '$lib/apis/auths';
const i18n = getContext('i18n');
let show = false;
let currentPassword = '';
let newPassword = '';
...
...
@@ -17,7 +20,7 @@
);
if (res) {
toast.success('Successfully updated.');
toast.success(
$i18n.t(
'Successfully updated.')
)
;
}
currentPassword = '';
...
...
@@ -40,20 +43,20 @@
}}
>
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">Change Password</div>
<div class=" font-medium">
{$i18n.t('
Change Password
')}
</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
show = !show;
}}>{show ? 'Hide' : 'Show'}</button
}}>{show ?
$i18n.t(
'Hide'
)
:
$i18n.t(
'Show'
)
}</button
>
</div>
{#if show}
<div class=" py-2.5 space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
Current Password
')}
</div>
<div class="flex-1">
<input
...
...
@@ -67,7 +70,7 @@
</div>
<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">
<input
...
...
@@ -81,7 +84,7 @@
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
Confirm Password
')}
</div>
<div class="flex-1">
<input
...
...
@@ -99,7 +102,7 @@
<button
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
>
Update password
{$i18n.t('
Update password
')}
</button>
</div>
{/if}
...
...
src/lib/components/chat/Settings/Advanced.svelte
View file @
621719c6
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import AdvancedParams from './Advanced/AdvancedParams.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import AdvancedParams from './Advanced/AdvancedParams.svelte';
export let saveSettings: Function;
// Advanced
...
...
@@ -55,14 +57,14 @@
<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=" text-sm font-medium">Parameters</div>
<div class=" text-sm font-medium">
{$i18n.t('
Parameters
')}
</div>
<AdvancedParams bind:options />
<hr class=" dark:border-gray-700" />
<div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Keep Alive</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Keep Alive
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -72,9 +74,9 @@
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('
Custom
')}
</span>
{/if}
</button>
</div>
...
...
@@ -84,7 +86,7 @@
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={
`e.g.) "30s","
10m
"
. Valid time units are
"s", "m", "h".`
}
placeholder={
$i18n.t("e.g. '30s','
10m
'
. Valid time units are
's', 'm', 'h'.")
}
bind:value={keepAlive}
/>
</div>
...
...
@@ -93,7 +95,7 @@
<div>
<div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Request Mode</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Request Mode
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -102,7 +104,7 @@
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else if requestFormat === 'json'}
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
...
...
@@ -114,7 +116,7 @@
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg> -->
<span class="ml-2 self-center">
JSON
</span>
<span class="ml-2 self-center">
{$i18n.t('
JSON
')}
</span>
{/if}
</button>
</div>
...
...
@@ -147,7 +149,7 @@
dispatch('save');
}}
>
Save
{$i18n.t('
Save
')}
</button>
</div>
</div>
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
View file @
621719c6
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let options = {
// Advanced
seed: 0,
...
...
@@ -20,7 +24,7 @@
<div class=" space-y-3 text-xs">
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Seed</div>
<div class=" w-20 text-xs font-medium self-center">
{$i18n.t('
Seed
')}
</div>
<div class=" flex-1 self-center">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
...
...
@@ -36,12 +40,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div>
<div class=" w-20 text-xs font-medium self-center">
{$i18n.t('
Stop Sequence
')}
</div>
<div class=" flex-1 self-center">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder=
"
Enter
S
top
S
equence
"
placeholder=
{$i18n.t('
Enter
s
top
s
equence
')}
bind:value={options.stop}
autocomplete="off"
/>
...
...
@@ -51,7 +55,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Temperature</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Temperature
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -61,9 +65,9 @@
}}
>
{#if options.temperature === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">
{$i18n.t('
Custom
')}
</span>
{/if}
</button>
</div>
...
...
@@ -97,7 +101,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Mirostat
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -107,9 +111,9 @@
}}
>
{#if options.mirostat === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -143,7 +147,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Mirostat Eta
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -153,9 +157,9 @@
}}
>
{#if options.mirostat_eta === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -189,7 +193,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Mirostat Tau
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -199,9 +203,9 @@
}}
>
{#if options.mirostat_tau === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('
Custom
')}
</span>
{/if}
</button>
</div>
...
...
@@ -235,7 +239,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top K</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Top K
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -245,9 +249,9 @@
}}
>
{#if options.top_k === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -281,7 +285,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top P</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Top P
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -291,9 +295,9 @@
}}
>
{#if options.top_p === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -327,7 +331,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Repeat Penalty
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -337,9 +341,9 @@
}}
>
{#if options.repeat_penalty === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -373,7 +377,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Last N</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Repeat Last N
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -383,9 +387,9 @@
}}
>
{#if options.repeat_last_n === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -419,7 +423,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Tfs Z</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Tfs Z
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -429,9 +433,9 @@
}}
>
{#if options.tfs_z === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -465,7 +469,7 @@
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Context Length</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Context Length
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -475,9 +479,9 @@
}}
>
{#if options.num_ctx === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
@@ -510,7 +514,7 @@
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Max Tokens</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Max Tokens
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -520,9 +524,9 @@
}}
>
{#if options.num_predict === ''}
<span class="ml-2 self-center">
Default
</span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center">
Custom
</span>
<span class="ml-2 self-center">
{$i18n.t('Default')}
</span>
{/if}
</button>
</div>
...
...
src/lib/components/chat/Settings/Audio.svelte
View file @
621719c6
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount
, getContext
} from 'svelte';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
// Audio
...
...
@@ -101,32 +103,36 @@
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div class=" mb-1 text-sm font-medium">STT Settings</div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('
STT Settings
')}
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Speech-to-Text Engine</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Speech-to-Text Engine
')}
</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
class="
dark:bg-gray-900
w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={STTEngine}
placeholder="Select a mode"
on:change={(e) => {
if (e.target.value !== '') {
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
toast.error(`Permission denied when accessing microphone: ${err}`);
toast.error(
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
error: err
})
);
STTEngine = '';
});
}
}}
>
<option value="">Default (Web API)</option>
<option value="whisper-local">Whisper (Local)</option>
<option value="">
{$i18n.t('
Default (Web API)
')}
</option>
<option value="whisper-local">
{$i18n.t('
Whisper (Local)
')}
</option>
</select>
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Conversation Mode</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Conversation Mode
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -136,15 +142,17 @@
type="button"
>
{#if conversationMode === true}
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Auto-send input after 3 sec.</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Auto-send input after 3 sec.')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -154,22 +162,22 @@
type="button"
>
{#if speechAutoSend === true}
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" mb-1 text-sm font-medium">TTS Settings</div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('
TTS Settings
')}
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Text-to-Speech Engine</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Text-to-Speech Engine
')}
</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
class="
dark:bg-gray-900
w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTSEngine}
placeholder="Select a mode"
on:change={(e) => {
...
...
@@ -182,14 +190,14 @@
}
}}
>
<option value="">Default (Web API)</option>
<option value="openai">Open AI</option>
<option value="">
{$i18n.t('
Default (Web API)
')}
</option>
<option value="openai">
{$i18n.t('
Open AI
')}
</option>
</select>
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Auto-playback response</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Auto-playback response
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -199,9 +207,9 @@
type="button"
>
{#if responseAutoPlayback === true}
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
...
...
@@ -211,7 +219,7 @@
{#if TTSEngine === ''}
<div>
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Set Voice
')}
</div>
<div class="flex w-full">
<div class="flex-1">
<select
...
...
@@ -219,7 +227,7 @@
bind:value={speaker}
placeholder="Select a voice"
>
<option value="" selected>Default</option>
<option value="" selected>
{$i18n.t('
Default
')}
</option>
{#each voices.filter((v) => v.localService === true) as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
>
...
...
@@ -230,7 +238,7 @@
</div>
{:else if TTSEngine === 'openai'}
<div>
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Set Voice
')}
</div>
<div class="flex w-full">
<div class="flex-1">
<select
...
...
@@ -254,7 +262,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('
Save
')}
</button>
</div>
</form>
src/lib/components/chat/Settings/Chats.svelte
View file @
621719c6
...
...
@@ -13,10 +13,12 @@
getChatList
} from '$lib/apis/chats';
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
import { onMount } from 'svelte';
import { onMount
, getContext
} from 'svelte';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let saveSettings: Function;
// Chats
let saveChatHistory = true;
...
...
@@ -99,13 +101,13 @@
});
</script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class="flex flex-col h-full justify-between space-y-3 text-sm
max-h-[22rem]
">
<div class=" space-y-2">
<div
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
>
<div class="flex w-full justify-between">
<div class=" self-center text-sm font-medium">Chat History</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Chat History
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -129,7 +131,7 @@
/>
</svg>
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
...
...
@@ -147,13 +149,13 @@
/>
</svg>
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
<div class="text-xs text-left w-full font-medium mt-0.5">
This setting does not sync across browsers or devices.
{$i18n.t('
This setting does not sync across browsers or devices.
')}
</div>
</div>
...
...
@@ -188,7 +190,7 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Import Chats</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Import Chats
')}
</div>
</button>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
...
...
@@ -210,7 +212,7 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export Chats</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Export Chats
')}
</div>
</button>
</div>
...
...
@@ -232,7 +234,7 @@
clip-rule="evenodd"
/>
</svg>
<span>Are you sure?</span>
<span>
{$i18n.t('
Are you sure?
')}
</span>
</div>
<div class="flex space-x-1.5 items-center">
...
...
@@ -296,7 +298,7 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Delete Chats</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Delete Chats
')}
</div>
</button>
{/if}
...
...
@@ -324,7 +326,9 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Export All Chats (All Users)')}
</div>
</button>
<hr class=" dark:border-gray-700" />
...
...
@@ -338,7 +342,7 @@
});
if (res) {
toast.success('Success');
toast.success(
$i18n.t(
'Success')
)
;
}
}}
>
...
...
@@ -356,7 +360,7 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Reset Vector Storage
')}
</div>
</button>
{/if}
</div>
...
...
src/lib/components/chat/Settings/Connections.svelte
View file @
621719c6
<script lang="ts">
import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount
, getContext
} from 'svelte';
const dispatch = createEventDispatcher();
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
...
...
@@ -12,6 +12,8 @@
} from '$lib/apis/openai';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let getModels: Function;
// External
...
...
@@ -42,7 +44,7 @@
});
if (ollamaVersion) {
toast.success('Server connection verified');
toast.success(
$i18n.t(
'Server connection verified')
)
;
await models.set(await getModels());
}
};
...
...
@@ -63,17 +65,17 @@
dispatch('save');
}}
>
<div class=" pr-1.5 overflow-y-scroll max-h-[2
0.5
rem] space-y-3">
<div class=" pr-1.5 overflow-y-scroll max-h-[2
2
rem] space-y-3">
<div class=" space-y-3">
<div class="mt-2 space-y-2 pr-1.5">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">OpenAI API</div>
<div class=" font-medium">
{$i18n.t('
OpenAI API
')}
</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showOpenAI = !showOpenAI;
}}>{showOpenAI ? 'Hide' : 'Show'}</button
}}>{showOpenAI ?
$i18n.t(
'Hide'
)
:
$i18n.t(
'Show'
)
}</button
>
</div>
...
...
@@ -84,7 +86,7 @@
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
API Base URL
"
placeholder=
{$i18n.t('
API Base URL
')}
bind:value={url}
autocomplete="off"
/>
...
...
@@ -93,7 +95,7 @@
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
API Key
"
placeholder=
{$i18n.t('
API Key
')}
bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off"
/>
...
...
@@ -143,7 +145,8 @@
</div>
</div>
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span>
{$i18n.t('WebUI will make requests to')}
<span class=" text-gray-200">'{url}/models'</span>
</div>
{/each}
</div>
...
...
@@ -154,7 +157,7 @@
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Ollama Base URL
')}
</div>
<div class="flex w-full gap-1.5">
<div class="flex-1 flex flex-col gap-2">
{#each OLLAMA_BASE_URLS as url, idx}
...
...
@@ -233,13 +236,13 @@
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Trouble accessing Ollama?
{$i18n.t('
Trouble accessing Ollama?
')}
<a
class=" text-gray-300 font-medium"
class=" text-gray-300 font-medium
underline
"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
Click here for help.
{$i18n.t('
Click here for help.
')}
</a>
</div>
</div>
...
...
@@ -250,7 +253,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('
Save
')}
</button>
</div>
</form>
src/lib/components/chat/Settings/General.svelte
View file @
621719c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { getLanguages } from '$lib/i18n';
const dispatch = createEventDispatcher();
import { models, user } from '$lib/stores';
import { models, user, theme } from '$lib/stores';
const i18n = getContext('i18n');
import AdvancedParams from './Advanced/AdvancedParams.svelte';
...
...
@@ -12,7 +15,10 @@
// General
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
let theme = 'dark';
let selectedTheme = 'system';
let languages = [];
let lang = $i18n.language;
let notificationEnabled = false;
let system = '';
...
...
@@ -63,9 +69,11 @@
};
onMount(async () => {
selectedTheme = localStorage.theme ?? 'system';
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
languages = await getLanguages();
theme = localStorage.theme ?? 'dark';
notificationEnabled = settings.notificationEnabled ?? false;
system = settings.system ?? '';
...
...
@@ -81,77 +89,93 @@
options = { ...options, ...settings.options };
options.stop = (settings?.options?.stop ?? []).join(',');
});
const applyTheme = (_theme: string) => {
let themeToApply = _theme;
if (_theme === 'system') {
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
themes
.filter((e) => e !== themeToApply)
.forEach((e) => {
e.split(' ').forEach((e) => {
document.documentElement.classList.remove(e);
});
});
themeToApply.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
});
console.log(_theme);
};
const themeChangeHandler = (_theme: string) => {
theme.set(_theme);
localStorage.setItem('theme', _theme);
applyTheme(_theme);
};
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class=" pr-1.5 overflow-y-scroll max-h-[2
0.5
rem]">
<div class=" pr-1.5 overflow-y-scroll max-h-[2
2
rem]">
<div class="">
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('
WebUI Settings
')}
</div>
<div class="
py-0.5
flex w-full justify-between">
<div class=" self-center text-xs font-medium">Theme</div>
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('
Theme
')}
</div>
<div class="flex items-center relative">
<div class=" absolute right-16">
{#if theme === 'dark'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
clip-rule="evenodd"
/>
</svg>
{:else if theme === 'light'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg>
{/if}
</div>
<select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={
t
heme}
class="
dark:bg-gray-900
w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={
selectedT
heme}
placeholder="Select a theme"
on:change={(e) => {
localStorage.theme = theme;
themes
.filter((e) => e !== theme)
.forEach((e) => {
e.split(' ').forEach((e) => {
document.documentElement.classList.remove(e);
});
});
theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
});
on:change={() => themeChangeHandler(selectedTheme)}
>
<option value="system">⚙️ {$i18n.t('System')}</option>
<option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
</select>
</div>
</div>
console.log(theme);
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
<div class="flex items-center relative">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={lang}
placeholder="Select a language"
on:change={(e) => {
$i18n.changeLanguage(lang);
}}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="rose-pine dark">Rosé Pine</option>
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
{#each languages as language}
<option value={language['code']}>{language['title']}</option>
{/each}
</select>
</div>
</div>
{#if $i18n.language === 'en-US'}
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
Couldn't find your language?
<a
class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
target="_blank"
>
Help us translate Open WebUI!
</a>
</div>
{/if}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Notification</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Desktop
Notification
s')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -161,9 +185,9 @@
type="button"
>
{#if notificationEnabled === true}
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
...
...
@@ -173,7 +197,7 @@
<hr class=" dark:border-gray-700 my-3" />
<div>
<div class=" my-2.5 text-sm font-medium">System Prompt</div>
<div class=" my-2.5 text-sm font-medium">
{$i18n.t('
System Prompt
')}
</div>
<textarea
bind:value={system}
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
...
...
@@ -183,13 +207,13 @@
<div class="mt-2 space-y-3 pr-1.5">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">Advanced Parameters</div>
<div class=" font-medium">
{$i18n.t('
Advanced Parameters
')}
</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showAdvanced = !showAdvanced;
}}>{showAdvanced ? 'Hide' : 'Show'}</button
}}>{showAdvanced ?
$i18n.t(
'Hide'
)
:
$i18n.t(
'Show'
)
}</button
>
</div>
...
...
@@ -199,7 +223,7 @@
<div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Keep Alive</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Keep Alive
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -209,9 +233,9 @@
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">
{$i18n.t('
Custom
')}
</span>
{/if}
</button>
</div>
...
...
@@ -221,7 +245,7 @@
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={
`e.g.) "30s","
10m
"
. Valid time units are
"s", "m", "h".`
}
placeholder={
$i18n.t("e.g. '30s','
10m
'
. Valid time units are
's', 'm', 'h'.")
}
bind:value={keepAlive}
/>
</div>
...
...
@@ -230,7 +254,7 @@
<div>
<div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Request Mode</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('
Request Mode
')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
...
...
@@ -239,7 +263,7 @@
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">
{$i18n.t('
Default
')}
</span>
{:else if requestFormat === 'json'}
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
...
...
@@ -251,7 +275,7 @@
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg> -->
<span class="ml-2 self-center"> JSON </span>
<span class="ml-2 self-center">
{$i18n.t('
JSON
')}
</span>
{/if}
</button>
</div>
...
...
@@ -286,7 +310,7 @@
dispatch('save');
}}
>
Save
{$i18n.t('
Save
')}
</button>
</div>
</div>
src/lib/components/chat/Settings/Images.svelte
View file @
621719c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount
, getContext
} from 'svelte';
import { config, user } from '$lib/stores';
import {
getAUTOMATIC1111Url,
...
...
@@ -21,6 +21,8 @@
import { getBackendConfig } from '$lib/apis';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let loading = false;
...
...
@@ -61,7 +63,7 @@
await getModels();
if (models) {
toast.success('Server connection verified');
toast.success(
$i18n.t(
'Server connection verified')
)
;
}
} else {
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
...
...
@@ -136,39 +138,41 @@
loading = false;
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
0.5
rem]">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
4
rem]">
<div>
<div class=" mb-1 text-sm font-medium">Image Settings</div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('
Image Settings
')}
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Image Generation Engine</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Image Generation Engine
')}
</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={imageGenerationEngine}
placeholder=
"
Select a mode
"
placeholder=
{$i18n.t('
Select a mode
')}
on:change={async () => {
await updateImageGeneration();
}}
>
<option value="">Default (Automatic1111)</option>
<option value="openai">Open AI (Dall-E)</option>
<option value="">
{$i18n.t('
Default (Automatic1111)
')}
</option>
<option value="openai">
{$i18n.t('
Open AI (Dall-E)
')}
</option>
</select>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Image Generation (Experimental)')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
toast.error('AUTOMATIC1111 Base URL is required.');
toast.error(
$i18n.t(
'AUTOMATIC1111 Base URL is required.')
)
;
enableImageGeneration = false;
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
toast.error('OpenAI API Key is required.');
toast.error(
$i18n.t(
'OpenAI API Key is required.')
)
;
enableImageGeneration = false;
} else {
enableImageGeneration = !enableImageGeneration;
...
...
@@ -179,9 +183,9 @@
type="button"
>
{#if enableImageGeneration === true}
<span class="ml-2 self-center">
On
</span>
<span class="ml-2 self-center">
{$i18n.t('On')}
</span>
{:else}
<span class="ml-2 self-center">
Off
</span>
<span class="ml-2 self-center">
{$i18n.t('Off')}
</span>
{/if}
</button>
</div>
...
...
@@ -190,12 +194,12 @@
<hr class=" dark:border-gray-700" />
{#if imageGenerationEngine === ''}
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
AUTOMATIC1111 Base URL
')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
Enter URL (e.g. http://127.0.0.1:7860/)
"
placeholder=
{$i18n.t('
Enter URL (e.g. http://127.0.0.1:7860/)
')}
bind:value={AUTOMATIC1111_BASE_URL}
/>
</div>
...
...
@@ -224,22 +228,22 @@
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Include `--api` flag when running stable-diffusion-webui
{$i18n.t('
Include `--api` flag when running stable-diffusion-webui
')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank"
>
(e.g. `sh webui.sh --api`)
{$i18n.t('
(e.g. `sh webui.sh --api`)
')}
</a>
</div>
{:else if imageGenerationEngine === 'openai'}
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
OpenAI API Key
')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
Enter API Key
"
placeholder=
{$i18n.t('
Enter API Key
')}
bind:value={OPENAI_API_KEY}
/>
</div>
...
...
@@ -250,16 +254,16 @@
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Set Default Model</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Set Default Model
')}
</div>
<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={selectedModel}
placeholder=
"
Select a model
"
placeholder=
{$i18n.t('
Select a model
')}
>
{#if !selectedModel}
<option value="" disabled selected>Select a model</option>
<option value="" disabled selected>
{$i18n.t('
Select a model
')}
</option>
{/if}
{#each models ?? [] as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
...
...
@@ -270,12 +274,12 @@
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Set Image Size
')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
Enter Image Size (e.g. 512x512)
"
placeholder=
{$i18n.t('
Enter Image Size (e.g. 512x512)
')}
bind:value={imageSize}
/>
</div>
...
...
@@ -283,12 +287,12 @@
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">Set Steps</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Set Steps
')}
</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
Enter Number of Steps (e.g. 50)
"
placeholder=
{$i18n.t('
Enter Number of Steps (e.g. 50)
')}
bind:value={steps}
/>
</div>
...
...
@@ -305,7 +309,7 @@
type="submit"
disabled={loading}
>
Save
{$i18n.t('
Save
')}
{#if loading}
<div class="ml-2 self-center">
...
...
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment