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
22c50f62
Unverified
Commit
22c50f62
authored
Apr 20, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Apr 20, 2024
Browse files
Merge pull request #1631 from open-webui/dev
0.1.120
parents
e0ebd7ae
eefe0145
Changes
50
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
820 additions
and
109 deletions
+820
-109
src/lib/components/chat/Messages/RateComment.svelte
src/lib/components/chat/Messages/RateComment.svelte
+37
-25
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+22
-5
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+14
-0
src/lib/components/chat/Settings/Audio.svelte
src/lib/components/chat/Settings/Audio.svelte
+60
-9
src/lib/components/chat/Settings/Interface.svelte
src/lib/components/chat/Settings/Interface.svelte
+3
-3
src/lib/components/chat/SettingsModal.svelte
src/lib/components/chat/SettingsModal.svelte
+2
-30
src/lib/components/chat/ShareChatModal.svelte
src/lib/components/chat/ShareChatModal.svelte
+18
-11
src/lib/components/common/Modal.svelte
src/lib/components/common/Modal.svelte
+4
-2
src/lib/components/documents/Settings/General.svelte
src/lib/components/documents/Settings/General.svelte
+9
-9
src/lib/components/icons/ArchiveBox.svelte
src/lib/components/icons/ArchiveBox.svelte
+19
-0
src/lib/components/icons/Share.svelte
src/lib/components/icons/Share.svelte
+11
-0
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+1
-1
src/lib/components/layout/Sidebar.svelte
src/lib/components/layout/Sidebar.svelte
+60
-10
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
+168
-0
src/lib/components/layout/Sidebar/ChatMenu.svelte
src/lib/components/layout/Sidebar/ChatMenu.svelte
+13
-1
src/lib/i18n/locales/bg-BG/translation.json
src/lib/i18n/locales/bg-BG/translation.json
+1
-1
src/lib/i18n/locales/ja-JP/translation.json
src/lib/i18n/locales/ja-JP/translation.json
+1
-1
src/lib/i18n/locales/ka-GE/translation.json
src/lib/i18n/locales/ka-GE/translation.json
+372
-0
src/lib/i18n/locales/ko-KR/translation.json
src/lib/i18n/locales/ko-KR/translation.json
+1
-1
src/lib/i18n/locales/languages.json
src/lib/i18n/locales/languages.json
+4
-0
No files found.
src/lib/components/chat/Messages/RateComment.svelte
View file @
22c50f62
<script lang="ts">
<script lang="ts">
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
export let messageId = null;
export let show = false;
export let show = false;
export let message;
export let message;
const LIKE_REASONS = [
let LIKE_REASONS = [];
`Accurate information`,
let DISLIKE_REASONS = [];
`Followed instructions perfectly`,
`Showcased creativity`,
function loadReasons() {
`Positive attitude`,
LIKE_REASONS = [
`Attention to detail`,
$i18n.t('Accurate information'),
`Thorough explanation`,
$i18n.t('Followed instructions perfectly'),
`Other`
$i18n.t('Showcased creativity'),
];
$i18n.t('Positive attitude'),
$i18n.t('Attention to detail'),
const DISLIKE_REASONS = [
$i18n.t('Thorough explanation'),
`Don't like the style`,
$i18n.t('Other')
`Not factually correct`,
];
`Didn't fully follow instructions`,
`Refused when it shouldn't have`,
DISLIKE_REASONS = [
`Being Lazy`,
$i18n.t("Don't like the style"),
`Other`
$i18n.t('Not factually correct'),
];
$i18n.t("Didn't fully follow instructions"),
$i18n.t("Refused when it shouldn't have"),
$i18n.t('Being lazy'),
$i18n.t('Other')
];
}
let reasons = [];
let reasons = [];
let selectedReason = null;
let selectedReason = null;
...
@@ -40,6 +48,7 @@
...
@@ -40,6 +48,7 @@
onMount(() => {
onMount(() => {
selectedReason = message.annotation.reason;
selectedReason = message.annotation.reason;
comment = message.annotation.comment;
comment = message.annotation.comment;
loadReasons();
});
});
const submitHandler = () => {
const submitHandler = () => {
...
@@ -50,14 +59,17 @@
...
@@ -50,14 +59,17 @@
dispatch('submit');
dispatch('submit');
toast.success('Thanks for your feedback!');
toast.success(
$i18n.t(
'Thanks for your feedback!')
)
;
show = false;
show = false;
};
};
</script>
</script>
<div class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850">
<div
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
id="message-feedback-{messageId}"
>
<div class="flex justify-between items-center">
<div class="flex justify-between items-center">
<div class=" text-sm">Tell us more:</div>
<div class=" text-sm">
{$i18n.t('
Tell us more:
')}
</div>
<button
<button
on:click={() => {
on:click={() => {
...
@@ -81,9 +93,9 @@
...
@@ -81,9 +93,9 @@
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
{#each reasons as reason}
{#each reasons as reason}
<button
<button
class="px-3.5 py-1 border dark:border-gray-850 dark:hover:bg-gray-850 {selectedReason ===
class="px-3.5 py-1 border dark:border-gray-850
hover:bg-gray-100
dark:hover:bg-gray-850 {selectedReason ===
reason
reason
? 'dark:bg-gray-800'
? '
bg-gray-200
dark:bg-gray-800'
: ''} transition rounded-lg"
: ''} transition rounded-lg"
on:click={() => {
on:click={() => {
selectedReason = reason;
selectedReason = reason;
...
@@ -99,7 +111,7 @@
...
@@ -99,7 +111,7 @@
<textarea
<textarea
bind:value={comment}
bind:value={comment}
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
placeholder=
"
Feel free to add specific details
"
placeholder=
{$i18n.t('
Feel free to add specific details
')}
rows="2"
rows="2"
/>
/>
</div>
</div>
...
...
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
22c50f62
...
@@ -15,7 +15,7 @@
...
@@ -15,7 +15,7 @@
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
import { config, settings } from '$lib/stores';
import { config, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/
openai
';
import { synthesizeOpenAISpeech } from '$lib/apis/
audio
';
import { imageGenerations } from '$lib/apis/images';
import { imageGenerations } from '$lib/apis/images';
import {
import {
approximateToHumanReadable,
approximateToHumanReadable,
...
@@ -176,10 +176,12 @@
...
@@ -176,10 +176,12 @@
const toggleSpeakMessage = async () => {
const toggleSpeakMessage = async () => {
if (speaking) {
if (speaking) {
speechSynthesis.cancel();
try {
speechSynthesis.cancel();
sentencesAudio[speakingIdx].pause();
sentencesAudio[speakingIdx].pause();
sentencesAudio[speakingIdx].currentTime = 0;
sentencesAudio[speakingIdx].currentTime = 0;
} catch {}
speaking = null;
speaking = null;
speakingIdx = null;
speakingIdx = null;
...
@@ -221,6 +223,10 @@
...
@@ -221,6 +223,10 @@
sentence
sentence
).catch((error) => {
).catch((error) => {
toast.error(error);
toast.error(error);
speaking = null;
loadingSpeech = false;
return null;
return null;
});
});
...
@@ -230,7 +236,6 @@
...
@@ -230,7 +236,6 @@
const audio = new Audio(blobUrl);
const audio = new Audio(blobUrl);
sentencesAudio[idx] = audio;
sentencesAudio[idx] = audio;
loadingSpeech = false;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
}
}
}
...
@@ -551,6 +556,12 @@
...
@@ -551,6 +556,12 @@
on:click={() => {
on:click={() => {
rateMessage(message.id, 1);
rateMessage(message.id, 1);
showRateComment = true;
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
}}
>
>
<svg
<svg
...
@@ -580,6 +591,11 @@
...
@@ -580,6 +591,11 @@
on:click={() => {
on:click={() => {
rateMessage(message.id, -1);
rateMessage(message.id, -1);
showRateComment = true;
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
}}
>
>
<svg
<svg
...
@@ -839,6 +855,7 @@
...
@@ -839,6 +855,7 @@
{#if showRateComment}
{#if showRateComment}
<RateComment
<RateComment
messageId={message.id}
bind:show={showRateComment}
bind:show={showRateComment}
bind:message
bind:message
on:submit={() => {
on:submit={() => {
...
...
src/lib/components/chat/Messages/UserMessage.svelte
View file @
22c50f62
...
@@ -176,10 +176,23 @@
...
@@ -176,10 +176,23 @@
e.target.style.height = '';
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
e.target.style.height = `${e.target.scrollHeight}px`;
}}
}}
on:keydown={(e) => {
if (e.key === 'Escape') {
document.getElementById('close-edit-message-button')?.click();
}
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
const isEnterPressed = e.key === 'Enter';
if (isCmdOrCtrlPressed && isEnterPressed) {
document.getElementById('save-edit-message-button')?.click();
}
}}
/>
/>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
<button
id="save-edit-message-button"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click={() => {
on:click={() => {
editMessageConfirmHandler();
editMessageConfirmHandler();
...
@@ -189,6 +202,7 @@
...
@@ -189,6 +202,7 @@
</button>
</button>
<button
<button
id="close-edit-message-button"
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => {
on:click={() => {
cancelEditMessage();
cancelEditMessage();
...
...
src/lib/components/chat/Settings/Audio.svelte
View file @
22c50f62
<script lang="ts">
<script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
import { user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
...
@@ -9,6 +11,9 @@
...
@@ -9,6 +11,9 @@
// Audio
// Audio
let OpenAIUrl = '';
let OpenAIKey = '';
let STTEngines = ['', 'openai'];
let STTEngines = ['', 'openai'];
let STTEngine = '';
let STTEngine = '';
...
@@ -69,6 +74,18 @@
...
@@ -69,6 +74,18 @@
saveSettings({ speechAutoSend: speechAutoSend });
saveSettings({ speechAutoSend: speechAutoSend });
};
};
const updateConfigHandler = async () => {
const res = await updateAudioConfig(localStorage.token, {
url: OpenAIUrl,
key: OpenAIKey
});
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
}
};
onMount(async () => {
onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
...
@@ -85,12 +102,24 @@
...
@@ -85,12 +102,24 @@
} else {
} else {
getWebAPIVoices();
getWebAPIVoices();
}
}
if ($user.role === 'admin') {
const res = await getAudioConfig(localStorage.token);
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
}
}
});
});
</script>
</script>
<form
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
on:submit|preventDefault={async () => {
if ($user.role === 'admin') {
await updateConfigHandler();
}
saveSettings({
saveSettings({
audio: {
audio: {
STTEngine: STTEngine !== '' ? STTEngine : undefined,
STTEngine: STTEngine !== '' ? STTEngine : undefined,
...
@@ -101,7 +130,7 @@
...
@@ -101,7 +130,7 @@
dispatch('save');
dispatch('save');
}}
}}
>
>
<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]
">
<div>
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
...
@@ -196,6 +225,26 @@
...
@@ -196,6 +225,26 @@
</div>
</div>
</div>
</div>
{#if $user.role === 'admin'}
{#if TTSEngine === 'openai'}
<div class="mt-1 flex gap-2 mb-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OpenAIKey}
required
/>
</div>
{/if}
{/if}
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
...
@@ -223,7 +272,7 @@
...
@@ -223,7 +272,7 @@
<div class="flex w-full">
<div class="flex w-full">
<div class="flex-1">
<div class="flex-1">
<select
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-8
0
0 outline-none"
class="w-full rounded
-lg
py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-8
5
0 outline-none"
bind:value={speaker}
bind:value={speaker}
placeholder="Select a voice"
placeholder="Select a voice"
>
>
...
@@ -241,16 +290,18 @@
...
@@ -241,16 +290,18 @@
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('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 w-full">
<div class="flex-1">
<div class="flex-1">
<select
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={speaker}
bind:value={speaker}
placeholder="Select a voice"
placeholder="Select a voice"
>
/>
<datalist id="voice-list">
{#each voices as voice}
{#each voices as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
<option value={voice.name} />
>
{/each}
{/each}
</
selec
t>
</
datalis
t>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/Settings/Interface.svelte
View file @
22c50f62
...
@@ -288,20 +288,20 @@
...
@@ -288,20 +288,20 @@
<div class="flex border-b dark:border-gray-600 w-full">
<div class="flex border-b dark:border-gray-600 w-full">
<input
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder=
"
Title (e.g. Tell me a fun fact)
"
placeholder=
{$i18n.t('
Title (e.g. Tell me a fun fact)
')}
bind:value={prompt.title[0]}
bind:value={prompt.title[0]}
/>
/>
<input
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder=
"
Subtitle (e.g. about the Roman Empire)
"
placeholder=
{$i18n.t('
Subtitle (e.g. about the Roman Empire)
')}
bind:value={prompt.title[1]}
bind:value={prompt.title[1]}
/>
/>
</div>
</div>
<input
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder=
"
Prompt (e.g. Tell me a fun fact about the Roman Empire)
"
placeholder=
{$i18n.t('
Prompt (e.g. Tell me a fun fact about the Roman Empire)
')}
bind:value={prompt.content}
bind:value={prompt.content}
/>
/>
</div>
</div>
...
...
src/lib/components/chat/SettingsModal.svelte
View file @
22c50f62
...
@@ -3,9 +3,7 @@
...
@@ -3,9 +3,7 @@
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores';
import { models, settings, user } from '$lib/stores';
import { getOllamaModels } from '$lib/apis/ollama';
import { getModels } from '$lib/utils';
import { getOpenAIModels } from '$lib/apis/openai';
import { getLiteLLMModels } from '$lib/apis/litellm';
import Modal from '../common/Modal.svelte';
import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte';
import Account from './Settings/Account.svelte';
...
@@ -25,37 +23,11 @@
...
@@ -25,37 +23,11 @@
const saveSettings = async (updated) => {
const saveSettings = async (updated) => {
console.log(updated);
console.log(updated);
await settings.set({ ...$settings, ...updated });
await settings.set({ ...$settings, ...updated });
await models.set(await getModels());
await models.set(await getModels(
localStorage.token
));
localStorage.setItem('settings', JSON.stringify($settings));
localStorage.setItem('settings', JSON.stringify($settings));
};
};
let selectedTab = 'general';
let selectedTab = 'general';
const getModels = async () => {
let models = await Promise.all([
await getOllamaModels(localStorage.token).catch((error) => {
console.log(error);
return null;
}),
await getOpenAIModels(localStorage.token).catch((error) => {
console.log(error);
return null;
}),
await getLiteLLMModels(localStorage.token).catch((error) => {
console.log(error);
return null;
})
]);
models = models
.filter((models) => models)
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
return models;
};
</script>
</script>
<Modal bind:show>
<Modal bind:show>
...
...
src/lib/components/chat/ShareChatModal.svelte
View file @
22c50f62
...
@@ -3,24 +3,27 @@
...
@@ -3,24 +3,27 @@
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import {
chatId,
modelfiles } from '$lib/stores';
import { modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils';
import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte';
import Modal from '../common/Modal.svelte';
import Link from '../icons/Link.svelte';
import Link from '../icons/Link.svelte';
export let chatId;
let chat = null;
let chat = null;
let shareUrl = null;
const i18n = getContext('i18n');
const i18n = getContext('i18n');
const shareLocalChat = async () => {
const shareLocalChat = async () => {
const _chat = chat;
const _chat = chat;
const sharedChat = await shareChatById(localStorage.token, $chatId);
const sharedChat = await shareChatById(localStorage.token, chatId);
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`;
shareUrl = `${window.location.origin}/s/${sharedChat.id}`;
console.log(shareUrl);
chat = await getChatById(localStorage.token, chatId);
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
return shareUrl;
copyToClipboard(chatShareUrl);
chat = await getChatById(localStorage.token, $chatId);
};
};
const shareChat = async () => {
const shareChat = async () => {
...
@@ -56,8 +59,8 @@
...
@@ -56,8 +59,8 @@
$: if (show) {
$: if (show) {
(async () => {
(async () => {
if (
$
chatId) {
if (chatId) {
chat = await getChatById(localStorage.token,
$
chatId);
chat = await getChatById(localStorage.token, chatId);
} else {
} else {
chat = null;
chat = null;
console.log(chat);
console.log(chat);
...
@@ -101,10 +104,10 @@
...
@@ -101,10 +104,10 @@
<button
<button
class="underline"
class="underline"
on:click={async () => {
on:click={async () => {
const res = await deleteSharedChatById(localStorage.token,
$
chatId);
const res = await deleteSharedChatById(localStorage.token, chatId);
if (res) {
if (res) {
chat = await getChatById(localStorage.token,
$
chatId);
chat = await getChatById(localStorage.token, chatId);
}
}
}}>delete this link</button
}}>delete this link</button
> and create a new shared link.
> and create a new shared link.
...
@@ -131,8 +134,12 @@
...
@@ -131,8 +134,12 @@
<button
<button
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button"
type="button"
on:
click
={() => {
on:
pointerdown
={() => {
shareLocalChat();
shareLocalChat();
}}
on:click={async () => {
copyToClipboard(shareUrl);
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
show = false;
show = false;
}}
}}
>
>
...
...
src/lib/components/common/Modal.svelte
View file @
22c50f62
...
@@ -15,8 +15,10 @@
...
@@ -15,8 +15,10 @@
return 'w-[16rem]';
return 'w-[16rem]';
} else if (size === 'sm') {
} else if (size === 'sm') {
return 'w-[30rem]';
return 'w-[30rem]';
} else {
} else
if (size === 'md')
{
return 'w-[44rem]';
return 'w-[44rem]';
} else {
return 'w-[48rem]';
}
}
};
};
...
@@ -47,7 +49,7 @@
...
@@ -47,7 +49,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
<div
bind:this={modalElement}
bind:this={modalElement}
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-
50
overflow-hidden overscroll-contain"
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-
[9999]
overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }}
in:fade={{ duration: 10 }}
on:click={() => {
on:click={() => {
show = false;
show = false;
...
...
src/lib/components/documents/Settings/General.svelte
View file @
22c50f62
...
@@ -29,8 +29,8 @@
...
@@ -29,8 +29,8 @@
let embeddingEngine = '';
let embeddingEngine = '';
let embeddingModel = '';
let embeddingModel = '';
let
o
penAIKey = '';
let
O
penAIKey = '';
let
o
penAIUrl = '';
let
O
penAIUrl = '';
let chunkSize = 0;
let chunkSize = 0;
let chunkOverlap = 0;
let chunkOverlap = 0;
...
@@ -79,7 +79,7 @@
...
@@ -79,7 +79,7 @@
return;
return;
}
}
if ((embeddingEngine === 'openai' &&
o
penAIKey === '') ||
o
penAIUrl === '') {
if ((embeddingEngine === 'openai' &&
O
penAIKey === '') ||
O
penAIUrl === '') {
toast.error($i18n.t('OpenAI URL/Key required.'));
toast.error($i18n.t('OpenAI URL/Key required.'));
return;
return;
}
}
...
@@ -93,8 +93,8 @@
...
@@ -93,8 +93,8 @@
...(embeddingEngine === 'openai'
...(embeddingEngine === 'openai'
? {
? {
openai_config: {
openai_config: {
key:
o
penAIKey,
key:
O
penAIKey,
url:
o
penAIUrl
url:
O
penAIUrl
}
}
}
}
: {})
: {})
...
@@ -133,8 +133,8 @@
...
@@ -133,8 +133,8 @@
embeddingEngine = embeddingConfig.embedding_engine;
embeddingEngine = embeddingConfig.embedding_engine;
embeddingModel = embeddingConfig.embedding_model;
embeddingModel = embeddingConfig.embedding_model;
o
penAIKey = embeddingConfig.openai_config.key;
O
penAIKey = embeddingConfig.openai_config.key;
o
penAIUrl = embeddingConfig.openai_config.url;
O
penAIUrl = embeddingConfig.openai_config.url;
}
}
};
};
...
@@ -192,14 +192,14 @@
...
@@ -192,14 +192,14 @@
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
placeholder={$i18n.t('API Base URL')}
bind:value={
o
penAIUrl}
bind:value={
O
penAIUrl}
required
required
/>
/>
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
placeholder={$i18n.t('API Key')}
bind:value={
o
penAIKey}
bind:value={
O
penAIKey}
required
required
/>
/>
</div>
</div>
...
...
src/lib/components/icons/ArchiveBox.svelte
0 → 100644
View file @
22c50f62
<script lang="ts">
export let className = 'size-3.5';
export let strokeWidth = '2.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"
/>
</svg>
src/lib/components/icons/Share.svelte
0 → 100644
View file @
22c50f62
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
src/lib/components/layout/Navbar.svelte
View file @
22c50f62
...
@@ -25,7 +25,7 @@
...
@@ -25,7 +25,7 @@
let showDownloadChatModal = false;
let showDownloadChatModal = false;
</script>
</script>
<ShareChatModal bind:show={showShareChatModal} />
<ShareChatModal bind:show={showShareChatModal}
chatId={$chatId}
/>
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div
<div
class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
...
...
src/lib/components/layout/Sidebar.svelte
View file @
22c50f62
...
@@ -17,13 +17,17 @@
...
@@ -17,13 +17,17 @@
getChatById,
getChatById,
getChatListByTagName,
getChatListByTagName,
updateChatById,
updateChatById,
getAllChatTags
getAllChatTags,
archiveChatById
} from '$lib/apis/chats';
} from '$lib/apis/chats';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { fade, slide } from 'svelte/transition';
import { fade, slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte';
import Tooltip from '../common/Tooltip.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
let show = false;
let show = false;
let navElement;
let navElement;
...
@@ -31,12 +35,16 @@
...
@@ -31,12 +35,16 @@
let title: string = 'UI';
let title: string = 'UI';
let search = '';
let search = '';
let shareChatId = null;
let selectedChatId = null;
let selectedChatId = null;
let chatDeleteId = null;
let chatDeleteId = null;
let chatTitleEditId = null;
let chatTitleEditId = null;
let chatTitle = '';
let chatTitle = '';
let showArchivedChatsModal = false;
let showShareChatModal = false;
let showDropdown = false;
let showDropdown = false;
let isEditing = false;
let isEditing = false;
...
@@ -134,8 +142,21 @@
...
@@ -134,8 +142,21 @@
localStorage.setItem('settings', JSON.stringify($settings));
localStorage.setItem('settings', JSON.stringify($settings));
location.href = '/';
location.href = '/';
};
};
const archiveChatHandler = async (id) => {
await archiveChatById(localStorage.token, id);
await chats.set(await getChatList(localStorage.token));
};
</script>
</script>
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ArchivedChatsModal
bind:show={showArchivedChatsModal}
on:change={async () => {
await chats.set(await getChatList(localStorage.token));
}}
/>
<div
<div
bind:this={navElement}
bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show
class="h-screen max-h-[100dvh] min-h-screen {show
...
@@ -544,9 +565,13 @@
...
@@ -544,9 +565,13 @@
</button>
</button>
</div>
</div>
{:else}
{:else}
<div class="flex self-center space-x-1
.5
z-10">
<div class="flex self-center space-x-1 z-10">
<ChatMenu
<ChatMenu
chatId={chat.id}
chatId={chat.id}
shareHandler={() => {
shareChatId = selectedChatId;
showShareChatModal = true;
}}
renameHandler={() => {
renameHandler={() => {
chatTitle = chat.title;
chatTitle = chat.title;
chatTitleEditId = chat.id;
chatTitleEditId = chat.id;
...
@@ -577,6 +602,18 @@
...
@@ -577,6 +602,18 @@
</svg>
</svg>
</button>
</button>
</ChatMenu>
</ChatMenu>
<Tooltip content="Archive">
<button
aria-label="Archive"
class=" self-center dark:hover:text-white transition"
on:click={() => {
archiveChatHandler(chat.id);
}}
>
<ArchiveBox />
</button>
</Tooltip>
</div>
</div>
{/if}
{/if}
</div>
</div>
...
@@ -609,13 +646,13 @@
...
@@ -609,13 +646,13 @@
{#if showDropdown}
{#if showDropdown}
<div
<div
id="dropdownDots"
id="dropdownDots"
class="absolute z-40 bottom-[70px]
4.5rem
rounded-
x
l shadow w-[240px] bg-white dark:bg-gray-900"
class="absolute z-40 bottom-[70px] rounded-l
g
shadow w-[240px] bg-white dark:bg-gray-900"
transition:fade|slide={{ duration: 100 }}
transition:fade|slide={{ duration: 100 }}
>
>
<div class="py-2 w-full">
<div class="
p-1
py-2 w-full">
{#if $user.role === 'admin'}
{#if $user.role === 'admin'}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/admin');
goto('/admin');
showDropdown = false;
showDropdown = false;
...
@@ -641,7 +678,7 @@
...
@@ -641,7 +678,7 @@
</button>
</button>
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/playground');
goto('/playground');
showDropdown = false;
showDropdown = false;
...
@@ -668,7 +705,20 @@
...
@@ -668,7 +705,20 @@
{/if}
{/if}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
showArchivedChatsModal = true;
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => {
on:click={async () => {
await showSettings.set(true);
await showSettings.set(true);
showDropdown = false;
showDropdown = false;
...
@@ -699,11 +749,11 @@
...
@@ -699,11 +749,11 @@
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-
7
00 m-0 p-0" />
<hr class=" dark:border-gray-
8
00 m-0 p-0" />
<div class="py-2 w-full">
<div class="
p-1
py-2 w-full">
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
localStorage.removeItem('token');
localStorage.removeItem('token');
location.href = '/auth';
location.href = '/auth';
...
...
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
0 → 100644
View file @
22c50f62
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Modal from '$lib/components/common/Modal.svelte';
import { archiveChatById, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let show = false;
let chats = [];
const unarchiveChatHandler = async (chatId) => {
const res = await archiveChatById(localStorage.token, chatId).catch((error) => {
toast.error(error);
});
chats = await getArchivedChatList(localStorage.token);
dispatch('change');
};
const deleteChatHandler = async (chatId) => {
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
toast.error(error);
});
chats = await getArchivedChatList(localStorage.token);
};
$: if (show) {
(async () => {
chats = await getArchivedChatList(localStorage.token);
})();
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Archived Chats')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-850" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
{#if chats.length > 0}
<div class="text-left text-sm w-full mb-4">
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto">
<thead
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 border-gray-800"
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2"> {$i18n.t('Created At')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody>
{#each chats as chat, idx}
<tr
class="bg-white {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
>
<td class="px-3 py-1 w-2/3">
<a href="/c/{chat.id}" target="_blank">
<div class=" underline line-clamp-1">
{chat.title}
</div>
</a>
</td>
<td class=" px-3 py-1">
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
<Tooltip content="Unarchive Chat">
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
unarchiveChatHandler(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 8.25H7.5a2.25 2.25 0 0 0-2.25 2.25v9a2.25 2.25 0 0 0 2.25 2.25h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25H15m0-3-3-3m0 0-3 3m3-3V15"
/>
</svg>
</button>
</Tooltip>
<Tooltip content="Delete Chat">
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
deleteChatHandler(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- {#each chats as chat}
<div>
{JSON.stringify(chat)}
</div>
{/each} -->
</div>
{:else}
<div class="text-left text-sm w-full mb-8">You have no archived conversations.</div>
{/if}
</div>
</div>
</div>
</Modal>
src/lib/components/layout/Sidebar/ChatMenu.svelte
View file @
22c50f62
...
@@ -7,7 +7,9 @@
...
@@ -7,7 +7,9 @@
import Pencil from '$lib/components/icons/Pencil.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
export let shareHandler: Function;
export let renameHandler: Function;
export let renameHandler: Function;
export let deleteHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
export let onClose: Function;
...
@@ -31,12 +33,22 @@
...
@@ -31,12 +33,22 @@
<div slot="content">
<div slot="content">
<DropdownMenu.Content
<DropdownMenu.Content
class="w-full max-w-[1
5
0px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
class="w-full max-w-[1
8
0px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
sideOffset={-2}
sideOffset={-2}
side="bottom"
side="bottom"
align="start"
align="start"
transition={flyAndScale}
transition={flyAndScale}
>
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">Share</div>
</DropdownMenu.Item>
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
on:click={() => {
...
...
src/lib/i18n/locales/bg-BG/translation.json
View file @
22c50f62
{
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
с
', '
м
', '
ч
', '
д
', '
с
' или '-1' за неограничен срок."
,
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
s
', '
m
', '
h
', '
d
', '
w
' или '-1' за неограничен срок."
,
"(Beta)"
:
"(Бета)"
,
"(Beta)"
:
"(Бета)"
,
"(e.g. `sh webui.sh --api`)"
:
"(например `sh webui.sh --api`)"
,
"(e.g. `sh webui.sh --api`)"
:
"(например `sh webui.sh --api`)"
,
"(latest)"
:
"(последна)"
,
"(latest)"
:
"(последна)"
,
...
...
src/lib/i18n/locales/ja-JP/translation.json
View file @
22c50f62
{
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
秒
', '
分
', '
時間
', '
日
', '
週
' または '-1' で無期限。"
,
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
s
', '
m
', '
h
', '
d
', '
w
' または '-1' で無期限。"
,
"(Beta)"
:
"(ベータ版)"
,
"(Beta)"
:
"(ベータ版)"
,
"(e.g. sh webui.sh --api)"
:
"(例: sh webui.sh --api)"
,
"(e.g. sh webui.sh --api)"
:
"(例: sh webui.sh --api)"
,
"(latest)"
:
"(最新)"
,
"(latest)"
:
"(最新)"
,
...
...
src/lib/i18n/locales/ka-GE/translation.json
0 → 100644
View file @
22c50f62
This diff is collapsed.
Click to expand it.
src/lib/i18n/locales/ko-KR/translation.json
View file @
22c50f62
{
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
초
', '
분
', '
시간
', '
일
', '
주
' 또는 만료 없음 '-1'"
,
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration."
:
"'
s
', '
m
', '
h
', '
d
', '
w
' 또는 만료 없음 '-1'"
,
"(Beta)"
:
"(Beta)"
,
"(Beta)"
:
"(Beta)"
,
"(e.g. `sh webui.sh --api`)"
:
"(예: `sh webui.sh --api`)"
,
"(e.g. `sh webui.sh --api`)"
:
"(예: `sh webui.sh --api`)"
,
"(latest)"
:
"(latest)"
,
"(latest)"
:
"(latest)"
,
...
...
src/lib/i18n/locales/languages.json
View file @
22c50f62
...
@@ -43,6 +43,10 @@
...
@@ -43,6 +43,10 @@
"code"
:
"ja-JP"
,
"code"
:
"ja-JP"
,
"title"
:
"Japanese"
"title"
:
"Japanese"
},
},
{
"code"
:
"ka-GE"
,
"title"
:
"Georgian"
},
{
{
"code"
:
"ko-KR"
,
"code"
:
"ko-KR"
,
"title"
:
"Korean"
"title"
:
"Korean"
...
...
Prev
1
2
3
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