feat: merge with main
This commit is contained in:
@@ -459,7 +459,7 @@ export const getChatById = async (token: string, id: string) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
@@ -580,7 +580,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const cloneChatById = async (token: string, id: string) => {
|
||||
export const cloneChatById = async (token: string, id: string, title?: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, {
|
||||
@@ -589,7 +589,10 @@ export const cloneChatById = async (token: string, id: string) => {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(title && { title: title })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
|
||||
@@ -58,6 +58,120 @@ export const exportConfig = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDirectConnectionsConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setDirectConnectionsConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...config
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getCodeExecutionConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setCodeExecutionConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...config
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getModelsConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
+114
-3
@@ -1,6 +1,11 @@
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { getOpenAIModelsDirect } from './openai';
|
||||
|
||||
export const getModels = async (token: string = '', base: boolean = false) => {
|
||||
export const getModels = async (
|
||||
token: string = '',
|
||||
connections: object | null = null,
|
||||
base: boolean = false
|
||||
) => {
|
||||
let error = null;
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
|
||||
method: 'GET',
|
||||
@@ -25,6 +30,111 @@ export const getModels = async (token: string = '', base: boolean = false) => {
|
||||
}
|
||||
|
||||
let models = res?.data ?? [];
|
||||
|
||||
if (connections && !base) {
|
||||
let localModels = [];
|
||||
|
||||
if (connections) {
|
||||
const OPENAI_API_BASE_URLS = connections.OPENAI_API_BASE_URLS;
|
||||
const OPENAI_API_KEYS = connections.OPENAI_API_KEYS;
|
||||
const OPENAI_API_CONFIGS = connections.OPENAI_API_CONFIGS;
|
||||
|
||||
const requests = [];
|
||||
for (const idx in OPENAI_API_BASE_URLS) {
|
||||
const url = OPENAI_API_BASE_URLS[idx];
|
||||
|
||||
if (idx.toString() in OPENAI_API_CONFIGS) {
|
||||
const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {};
|
||||
|
||||
const enable = apiConfig?.enable ?? true;
|
||||
const modelIds = apiConfig?.model_ids ?? [];
|
||||
|
||||
if (enable) {
|
||||
if (modelIds.length > 0) {
|
||||
const modelList = {
|
||||
object: 'list',
|
||||
data: modelIds.map((modelId) => ({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
owned_by: 'openai',
|
||||
openai: { id: modelId },
|
||||
urlIdx: idx
|
||||
}))
|
||||
};
|
||||
|
||||
requests.push(
|
||||
(async () => {
|
||||
return modelList;
|
||||
})()
|
||||
);
|
||||
} else {
|
||||
requests.push(
|
||||
(async () => {
|
||||
return await getOpenAIModelsDirect(url, OPENAI_API_KEYS[idx])
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
object: 'list',
|
||||
data: [],
|
||||
urlIdx: idx
|
||||
};
|
||||
});
|
||||
})()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
requests.push(
|
||||
(async () => {
|
||||
return {
|
||||
object: 'list',
|
||||
data: [],
|
||||
urlIdx: idx
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
for (const idx in responses) {
|
||||
const response = responses[idx];
|
||||
const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {};
|
||||
|
||||
let models = Array.isArray(response) ? response : (response?.data ?? []);
|
||||
models = models.map((model) => ({ ...model, openai: { id: model.id }, urlIdx: idx }));
|
||||
|
||||
const prefixId = apiConfig.prefix_id;
|
||||
if (prefixId) {
|
||||
for (const model of models) {
|
||||
model.id = `${prefixId}.${model.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
localModels = localModels.concat(models);
|
||||
}
|
||||
}
|
||||
|
||||
models = models.concat(
|
||||
localModels.map((model) => ({
|
||||
...model,
|
||||
name: model?.name ?? model?.id,
|
||||
direct: true
|
||||
}))
|
||||
);
|
||||
|
||||
// Remove duplicates
|
||||
const modelsMap = {};
|
||||
for (const model of models) {
|
||||
modelsMap[model.id] = model;
|
||||
}
|
||||
|
||||
models = Object.values(modelsMap);
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
@@ -880,13 +990,14 @@ export const getChangelog = async () => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getVersionUpdates = async () => {
|
||||
export const getVersionUpdates = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -219,7 +219,7 @@ export const deleteModelById = async (token: string, id: string) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
|
||||
@@ -208,6 +208,33 @@ export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
|
||||
return res.OPENAI_API_KEYS;
|
||||
};
|
||||
|
||||
export const getOpenAIModelsDirect = async (url: string, key: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${url}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(key && { authorization: `Bearer ${key}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||
return [];
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getOpenAIModels = async (token: string, urlIdx?: number) => {
|
||||
let error = null;
|
||||
|
||||
@@ -241,33 +268,62 @@ export const getOpenAIModels = async (token: string, urlIdx?: number) => {
|
||||
export const verifyOpenAIConnection = async (
|
||||
token: string = '',
|
||||
url: string = 'https://api.openai.com/v1',
|
||||
key: string = ''
|
||||
key: string = '',
|
||||
direct: boolean = false
|
||||
) => {
|
||||
if (!url) {
|
||||
throw 'OpenAI: URL is required';
|
||||
}
|
||||
|
||||
let error = null;
|
||||
let res = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
key
|
||||
if (direct) {
|
||||
res = await fetch(`${url}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${key}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||
return [];
|
||||
});
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||
return [];
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
res = await fetch(`${OPENAI_API_BASE_URL}/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
key
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||
return [];
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -322,7 +378,7 @@ export const generateOpenAIChatCompletion = async (
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = `${err?.detail ?? 'Network Problem'}`;
|
||||
error = `${err?.detail ?? err}`;
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -32,9 +32,15 @@ type ChunkConfigForm = {
|
||||
chunk_overlap: number;
|
||||
};
|
||||
|
||||
type DocumentIntelligenceConfigForm = {
|
||||
key: string;
|
||||
endpoint: string;
|
||||
};
|
||||
|
||||
type ContentExtractConfigForm = {
|
||||
engine: string;
|
||||
tika_server_url: string | null;
|
||||
document_intelligence_config: DocumentIntelligenceConfigForm | null;
|
||||
};
|
||||
|
||||
type YoutubeConfigForm = {
|
||||
@@ -46,6 +52,7 @@ type YoutubeConfigForm = {
|
||||
type RAGConfigForm = {
|
||||
pdf_extract_images?: boolean;
|
||||
enable_google_drive_integration?: boolean;
|
||||
enable_onedrive_integration?: boolean;
|
||||
chunk?: ChunkConfigForm;
|
||||
content_extraction?: ContentExtractConfigForm;
|
||||
web_loader_ssl_verification?: boolean;
|
||||
|
||||
@@ -284,14 +284,16 @@ export const updateUserInfo = async (token: string, info: object) => {
|
||||
|
||||
export const getAndUpdateUserLocation = async (token: string) => {
|
||||
const location = await getUserPosition().catch((err) => {
|
||||
throw err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (location) {
|
||||
await updateUserInfo(token, { location: location });
|
||||
return location;
|
||||
} else {
|
||||
throw new Error('Failed to get user location');
|
||||
console.log('Failed to get user location');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getGravatarUrl = async (email: string) => {
|
||||
export const getGravatarUrl = async (token: string, email: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -22,13 +23,14 @@ export const getGravatarUrl = async (email: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const formatPythonCode = async (code: string) => {
|
||||
export const executeCode = async (token: string, code: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: code
|
||||
@@ -55,13 +57,48 @@ export const formatPythonCode = async (code: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadChatAsPDF = async (title: string, messages: object[]) => {
|
||||
export const formatPythonCode = async (token: string, code: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: code
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
error = err;
|
||||
if (err.detail) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => {
|
||||
let error = null;
|
||||
|
||||
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
@@ -81,13 +118,14 @@ export const downloadChatAsPDF = async (title: string, messages: object[]) => {
|
||||
return blob;
|
||||
};
|
||||
|
||||
export const getHTMLFromMarkdown = async (md: string) => {
|
||||
export const getHTMLFromMarkdown = async (token: string, md: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
md: md
|
||||
|
||||
+13
-9
@@ -20,7 +20,9 @@
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
|
||||
export let ollama = false;
|
||||
export let direct = false;
|
||||
|
||||
export let connection = null;
|
||||
|
||||
@@ -46,9 +48,11 @@
|
||||
};
|
||||
|
||||
const verifyOpenAIHandler = async () => {
|
||||
const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
const res = await verifyOpenAIConnection(localStorage.token, url, key, direct).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
@@ -165,7 +169,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
@@ -198,7 +202,7 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex flex-col flex-shrink-0 self-end">
|
||||
<div class="flex flex-col shrink-0 self-end">
|
||||
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch bind:state={enable} />
|
||||
</Tooltip>
|
||||
@@ -211,7 +215,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<SensitiveInput
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={key}
|
||||
placeholder={$i18n.t('API Key')}
|
||||
required={!ollama}
|
||||
@@ -229,7 +233,7 @@
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={prefixId}
|
||||
placeholder={$i18n.t('Prefix ID')}
|
||||
@@ -254,7 +258,7 @@
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{modelId}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -288,7 +292,7 @@
|
||||
<input
|
||||
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
|
||||
? ''
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={modelId}
|
||||
placeholder={$i18n.t('Add a model ID')}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@
|
||||
v{version} - {changelog[version].date}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-2" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
||||
<div class="">
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-50 dark:border-gray-800 rounded-xl px-3.5 py-3.5"
|
||||
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-850 rounded-xl px-3.5 py-3.5"
|
||||
on:click={() => {
|
||||
onClick();
|
||||
dispatch('closeToast');
|
||||
}}
|
||||
>
|
||||
<div class="flex-shrink-0 self-top -translate-y-0.5">
|
||||
<div class="shrink-0 self-top -translate-y-0.5">
|
||||
<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
@@ -10,6 +10,32 @@
|
||||
|
||||
export let show = true;
|
||||
export let getStartedHandler = () => {};
|
||||
|
||||
function setLogoImage() {
|
||||
const logo = document.getElementById('logo');
|
||||
|
||||
if (logo) {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
if (isDarkMode) {
|
||||
const darkImage = new Image();
|
||||
darkImage.src = '/static/favicon-dark.png';
|
||||
|
||||
darkImage.onload = () => {
|
||||
logo.src = '/static/favicon-dark.png';
|
||||
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
||||
};
|
||||
|
||||
darkImage.onerror = () => {
|
||||
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
setLogoImage();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
@@ -18,6 +44,7 @@
|
||||
<div class="flex space-x-2">
|
||||
<div class=" self-center">
|
||||
<img
|
||||
id="logo"
|
||||
crossorigin="anonymous"
|
||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||
class=" w-6 rounded-full"
|
||||
@@ -30,10 +57,10 @@
|
||||
<SlideShow duration={5000} />
|
||||
|
||||
<div
|
||||
class="w-full h-full absolute top-0 left-0 bg-gradient-to-t from-20% from-black to-transparent"
|
||||
class="w-full h-full absolute top-0 left-0 bg-linear-to-t from-20% from-black to-transparent"
|
||||
></div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 backdrop-blur-sm bg-black/50"></div>
|
||||
<div class="w-full h-full absolute top-0 left-0 backdrop-blur-xs bg-black/50"></div>
|
||||
|
||||
<div class="relative bg-transparent w-full min-h-screen flex z-10">
|
||||
<div class="flex flex-col justify-end w-full items-center pb-10 text-center">
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
};
|
||||
|
||||
const shareHandler = async () => {
|
||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
|
||||
// remove snapshot from feedbacks
|
||||
const feedbacksToShare = feedbacks.map((f) => {
|
||||
@@ -131,14 +131,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
||||
<div
|
||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
||||
>
|
||||
{#if (feedbacks ?? []).length === 0}
|
||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||
{$i18n.t('No feedbacks found')}
|
||||
</div>
|
||||
{:else}
|
||||
<table
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
|
||||
>
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
||||
@@ -169,7 +171,7 @@
|
||||
<td class=" py-0.5 text-right font-semibold">
|
||||
<div class="flex justify-center">
|
||||
<Tooltip content={feedback?.user?.name}>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={feedback?.user?.profile_image_url ?? '/user.png'}
|
||||
alt={feedback?.user?.name}
|
||||
@@ -266,7 +268,7 @@
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium line-clamp-1">
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import * as ort from 'onnxruntime-web';
|
||||
import { AutoModel, AutoTokenizer } from '@huggingface/transformers';
|
||||
import { env, AutoModel, AutoTokenizer } from '@huggingface/transformers';
|
||||
|
||||
env.backends.onnx.wasm.wasmPaths = '/wasm/';
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
@@ -286,7 +288,7 @@
|
||||
<MagnifyingGlass className="size-3" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search')}
|
||||
on:focus={() => {
|
||||
@@ -298,7 +300,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
||||
<div
|
||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
||||
>
|
||||
{#if loadingLeaderboard}
|
||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||
<div class="m-auto">
|
||||
@@ -347,7 +351,7 @@
|
||||
</td>
|
||||
<td class="px-3 py-1.5 flex flex-col justify-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
|
||||
alt={model.name}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -65,7 +65,7 @@
|
||||
return null;
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
@@ -126,7 +126,12 @@
|
||||
toast.success($i18n.t('Function deleted successfully'));
|
||||
|
||||
functions.set(await getFunctions(localStorage.token));
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +152,12 @@
|
||||
}
|
||||
|
||||
functions.set(await getFunctions(localStorage.token));
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,12 +180,12 @@
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
window.addEventListener('blur', onBlur);
|
||||
window.addEventListener('blur-sm', onBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
window.removeEventListener('blur-sm', onBlur);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -201,7 +211,7 @@
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Functions')}
|
||||
/>
|
||||
@@ -231,14 +241,14 @@
|
||||
<div class=" flex-1 self-center pl-1">
|
||||
<div class=" font-semibold flex items-center gap-1.5">
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{func.type}
|
||||
</div>
|
||||
|
||||
{#if func?.meta?.manifest?.version}
|
||||
<div
|
||||
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
class="text-xs font-bold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
v{func?.meta?.manifest?.version ?? ''}
|
||||
</div>
|
||||
@@ -250,7 +260,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1.5 px-1">
|
||||
<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div>
|
||||
<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
|
||||
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{func.meta.description}
|
||||
@@ -359,7 +369,13 @@
|
||||
bind:state={func.is_active}
|
||||
on:change={async (e) => {
|
||||
toggleFunctionById(localStorage.token, func.id);
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -453,7 +469,7 @@
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||
{$i18n.t('Made by OpenWebUI Community')}
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
@@ -496,7 +512,12 @@
|
||||
id={selectedFunction?.id ?? null}
|
||||
on:save={async () => {
|
||||
await tick();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -517,7 +538,12 @@
|
||||
|
||||
toast.success($i18n.t('Functions imported successfully'));
|
||||
functions.set(await getFunctions(localStorage.token));
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
reader.readAsText(importFiles[0]);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, tick } from 'svelte';
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
@@ -15,6 +14,8 @@
|
||||
let loading = false;
|
||||
let showConfirm = false;
|
||||
|
||||
export let onSave = () => {};
|
||||
|
||||
export let edit = false;
|
||||
export let clone = false;
|
||||
|
||||
@@ -256,7 +257,7 @@ class Pipe:
|
||||
|
||||
const saveHandler = async () => {
|
||||
loading = true;
|
||||
dispatch('save', {
|
||||
onSave({
|
||||
id,
|
||||
name,
|
||||
meta,
|
||||
@@ -300,7 +301,7 @@ class Pipe:
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
||||
<div class="flex w-full items-center">
|
||||
<div class=" flex-shrink-0 mr-2">
|
||||
<div class=" shrink-0 mr-2">
|
||||
<Tooltip content={$i18n.t('Back')}>
|
||||
<button
|
||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
||||
@@ -317,7 +318,7 @@ class Pipe:
|
||||
<div class="flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
||||
<input
|
||||
class="w-full text-2xl font-medium bg-transparent outline-none font-primary"
|
||||
class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function Name')}
|
||||
bind:value={name}
|
||||
@@ -333,13 +334,13 @@ class Pipe:
|
||||
|
||||
<div class=" flex gap-2 px-1 items-center">
|
||||
{#if edit}
|
||||
<div class="text-sm text-gray-500 flex-shrink-0">
|
||||
<div class="text-sm text-gray-500 shrink-0">
|
||||
{id}
|
||||
</div>
|
||||
{:else}
|
||||
<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
|
||||
<input
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function ID')}
|
||||
bind:value={id}
|
||||
@@ -355,7 +356,7 @@ class Pipe:
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Function Description')}
|
||||
bind:value={meta.description}
|
||||
@@ -371,10 +372,10 @@ class Pipe:
|
||||
value={content}
|
||||
lang="python"
|
||||
{boilerplate}
|
||||
on:change={(e) => {
|
||||
_content = e.detail.value;
|
||||
onChange={(e) => {
|
||||
_content = e;
|
||||
}}
|
||||
on:save={async () => {
|
||||
onSave={async () => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
@@ -122,7 +122,7 @@
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import ChartBar from '../icons/ChartBar.svelte';
|
||||
import DocumentChartBar from '../icons/DocumentChartBar.svelte';
|
||||
import Evaluations from './Settings/Evaluations.svelte';
|
||||
import CodeExecution from './Settings/CodeExecution.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -188,6 +189,32 @@
|
||||
<div class=" self-center">{$i18n.t('Web Search')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'code-execution'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'code-execution';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm2.22 1.97a.75.75 0 0 0 0 1.06l.97.97-.97.97a.75.75 0 1 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06l-1.5-1.5a.75.75 0 0 0-1.06 0ZM8.75 8.5a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Code Execution')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'interface'
|
||||
@@ -364,6 +391,15 @@
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'code-execution'}
|
||||
<CodeExecution
|
||||
saveHandler={async () => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
|
||||
await tick();
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'interface'}
|
||||
<Interface
|
||||
on:save={() => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
getModels as _getModels,
|
||||
getVoices as _getVoices
|
||||
} from '$lib/apis/audio';
|
||||
import { config } from '$lib/stores';
|
||||
import { config, settings } from '$lib/stores';
|
||||
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
let STT_ENGINE = '';
|
||||
let STT_MODEL = '';
|
||||
let STT_WHISPER_MODEL = '';
|
||||
let STT_DEEPGRAM_API_KEY = '';
|
||||
|
||||
let STT_WHISPER_MODEL_LOADING = false;
|
||||
|
||||
@@ -50,8 +51,11 @@
|
||||
if (TTS_ENGINE === '') {
|
||||
models = [];
|
||||
} else {
|
||||
const res = await _getModels(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
const res = await _getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@@ -74,7 +78,7 @@
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@@ -103,7 +107,8 @@
|
||||
OPENAI_API_KEY: STT_OPENAI_API_KEY,
|
||||
ENGINE: STT_ENGINE,
|
||||
MODEL: STT_MODEL,
|
||||
WHISPER_MODEL: STT_WHISPER_MODEL
|
||||
WHISPER_MODEL: STT_WHISPER_MODEL,
|
||||
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,6 +148,7 @@
|
||||
STT_ENGINE = res.stt.ENGINE;
|
||||
STT_MODEL = res.stt.MODEL;
|
||||
STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
|
||||
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
|
||||
}
|
||||
|
||||
await getVoices();
|
||||
@@ -166,13 +172,14 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={STT_ENGINE}
|
||||
placeholder="Select an engine"
|
||||
>
|
||||
<option value="">{$i18n.t('Whisper (Local)')}</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
<option value="deepgram">Deepgram</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,7 +188,7 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full bg-transparent outline-none"
|
||||
class="flex-1 w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={STT_OPENAI_API_BASE_URL}
|
||||
required
|
||||
@@ -191,7 +198,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||
@@ -199,7 +206,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={STT_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
@@ -210,6 +217,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if STT_ENGINE === 'deepgram'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_DEEPGRAM_API_KEY} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={STT_MODEL}
|
||||
placeholder="Select a model (optional)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Leave model field empty to use the default model.')}
|
||||
<a
|
||||
class=" hover:underline dark:text-gray-200 text-gray-800"
|
||||
href="https://developers.deepgram.com/docs/models"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here to see available models.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else if STT_ENGINE === ''}
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||
@@ -217,7 +255,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Set whisper model')}
|
||||
bind:value={STT_WHISPER_MODEL}
|
||||
/>
|
||||
@@ -295,7 +333,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800" />
|
||||
<hr class="border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||
@@ -304,7 +342,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={TTS_ENGINE}
|
||||
placeholder="Select a mode"
|
||||
on:change={async (e) => {
|
||||
@@ -334,7 +372,7 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full bg-transparent outline-none"
|
||||
class="flex-1 w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||
required
|
||||
@@ -347,7 +385,7 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_API_KEY}
|
||||
required
|
||||
@@ -358,13 +396,13 @@
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_API_KEY}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Azure Region')}
|
||||
bind:value={TTS_AZURE_SPEECH_REGION}
|
||||
required
|
||||
@@ -373,7 +411,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
{#if TTS_ENGINE === ''}
|
||||
<div>
|
||||
@@ -381,7 +419,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_VOICE}
|
||||
>
|
||||
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
||||
@@ -404,7 +442,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="CMU ARCTIC speaker embedding name"
|
||||
/>
|
||||
@@ -446,7 +484,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
@@ -465,7 +503,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
@@ -487,7 +525,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
@@ -506,7 +544,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
@@ -528,7 +566,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
@@ -555,7 +593,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
|
||||
placeholder="Select a output format"
|
||||
/>
|
||||
@@ -565,13 +603,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="dark:border-gray-850 my-2" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="pt-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
aria-label="Select how to split message text for TTS requests"
|
||||
bind:value={TTS_SPLIT_ON}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs';
|
||||
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let config = null;
|
||||
|
||||
let engines = ['pyodide', 'jupyter'];
|
||||
|
||||
const submitHandler = async () => {
|
||||
const res = await setCodeExecutionConfig(localStorage.token, config);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const res = await getCodeExecutionConfig(localStorage.token);
|
||||
|
||||
if (res) {
|
||||
config = res;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
await submitHandler();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if config}
|
||||
<div>
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={config.CODE_EXECUTION_ENGINE}
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each engines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
|
||||
<div class="text-gray-500 text-xs">
|
||||
{$i18n.t(
|
||||
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
|
||||
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Jupyter URL')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter URL')}
|
||||
bind:value={config.CODE_EXECUTION_JUPYTER_URL}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||
<div class=" flex gap-2 w-full items-center justify-between">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Jupyter Auth')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
|
||||
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
|
||||
placeholder={$i18n.t('Select an auth method')}
|
||||
>
|
||||
<option selected value="">{$i18n.t('None')}</option>
|
||||
<option value="token">{$i18n.t('Token')}</option>
|
||||
<option value="password">{$i18n.t('Password')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.CODE_EXECUTION_JUPYTER_AUTH}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter Password')}
|
||||
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{:else}
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter Token')}
|
||||
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 w-full items-center justify-between">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Code Execution Timeout')}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
|
||||
<input
|
||||
class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
type="number"
|
||||
bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
|
||||
placeholder={$i18n.t('e.g. 60')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Enable Code Interpreter')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.ENABLE_CODE_INTERPRETER}
|
||||
<div class="mb-2.5">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Code Interpreter Engine')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={config.CODE_INTERPRETER_ENGINE}
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each engines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
|
||||
<div class="text-gray-500 text-xs">
|
||||
{$i18n.t(
|
||||
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
|
||||
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Jupyter URL')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter URL')}
|
||||
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
|
||||
<div class="flex gap-2 w-full items-center justify-between">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Jupyter Auth')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
|
||||
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
|
||||
placeholder={$i18n.t('Select an auth method')}
|
||||
>
|
||||
<option selected value="">{$i18n.t('None')}</option>
|
||||
<option value="token">{$i18n.t('Token')}</option>
|
||||
<option value="password">{$i18n.t('Password')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter Password')}
|
||||
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{:else}
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Jupyter Token')}
|
||||
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 w-full items-center justify-between">
|
||||
<div class="text-xs font-medium">
|
||||
{$i18n.t('Code Execution Timeout')}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Enter timeout in seconds')}>
|
||||
<input
|
||||
class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
type="number"
|
||||
bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
|
||||
placeholder={$i18n.t('e.g. 60')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 w-full">
|
||||
<div class=" mb-2.5 text-xs font-medium">
|
||||
{$i18n.t('Code Interpreter Prompt Template')}
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -7,8 +7,9 @@
|
||||
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
|
||||
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
|
||||
|
||||
import { models, user } from '$lib/stores';
|
||||
import { config, models, settings, user } from '$lib/stores';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
@@ -16,13 +17,16 @@
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
|
||||
import OpenAIConnection from './Connections/OpenAIConnection.svelte';
|
||||
import AddConnectionModal from './Connections/AddConnectionModal.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
import OllamaConnection from './Connections/OllamaConnection.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const getModels = async () => {
|
||||
const models = await _getModels(localStorage.token);
|
||||
const models = await _getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
);
|
||||
return models;
|
||||
};
|
||||
|
||||
@@ -37,6 +41,8 @@
|
||||
let ENABLE_OPENAI_API: null | boolean = null;
|
||||
let ENABLE_OLLAMA_API: null | boolean = null;
|
||||
|
||||
let directConnectionsConfig = null;
|
||||
|
||||
let pipelineUrls = {};
|
||||
let showAddOpenAIConnectionModal = false;
|
||||
let showAddOllamaConnectionModal = false;
|
||||
@@ -98,17 +104,33 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateDirectConnectionsHandler = async () => {
|
||||
const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Direct Connections settings updated'));
|
||||
await models.set(await getModels());
|
||||
}
|
||||
};
|
||||
|
||||
const addOpenAIConnectionHandler = async (connection) => {
|
||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
|
||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
|
||||
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config;
|
||||
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
|
||||
|
||||
await updateOpenAIHandler();
|
||||
};
|
||||
|
||||
const addOllamaConnectionHandler = async (connection) => {
|
||||
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
|
||||
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config;
|
||||
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
|
||||
...connection.config,
|
||||
key: connection.key
|
||||
};
|
||||
|
||||
await updateOllamaHandler();
|
||||
};
|
||||
@@ -124,6 +146,9 @@
|
||||
})(),
|
||||
(async () => {
|
||||
openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
@@ -167,6 +192,14 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const submitHandler = async () => {
|
||||
updateOpenAIHandler();
|
||||
updateOllamaHandler();
|
||||
updateDirectConnectionsHandler();
|
||||
|
||||
dispatch('save');
|
||||
};
|
||||
</script>
|
||||
|
||||
<AddConnectionModal
|
||||
@@ -180,17 +213,9 @@
|
||||
onSubmit={addOllamaConnectionHandler}
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateOpenAIHandler();
|
||||
updateOllamaHandler();
|
||||
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
|
||||
<div class="my-2">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
@@ -209,7 +234,7 @@
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -244,7 +269,12 @@
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
|
||||
delete OPENAI_API_CONFIGS[idx];
|
||||
let newConfig = {};
|
||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OPENAI_API_CONFIGS = newConfig;
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
@@ -254,7 +284,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm mb-2">
|
||||
@@ -271,7 +301,7 @@
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -302,7 +332,12 @@
|
||||
}}
|
||||
onDelete={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
delete OLLAMA_API_CONFIGS[idx];
|
||||
|
||||
let newConfig = {};
|
||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OLLAMA_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
@@ -322,6 +357,33 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||
on:change={async () => {
|
||||
updateDirectConnectionsHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t(
|
||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div
|
||||
class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
|
||||
>
|
||||
<div class=" flex-shrink-0">
|
||||
<div class=" shrink-0">
|
||||
{$i18n.t('Manage Ollama')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import AddConnectionModal from './AddConnectionModal.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||
@@ -56,7 +56,7 @@
|
||||
{/if}
|
||||
|
||||
<input
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||
bind:value={url}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import AddConnectionModal from './AddConnectionModal.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
import { connect } from 'socket.io-client';
|
||||
|
||||
export let onDelete = () => {};
|
||||
@@ -53,7 +54,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
||||
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
@@ -84,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
inputClassName=" outline-none bg-transparent w-full"
|
||||
inputClassName=" outline-hidden bg-transparent w-full"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={key}
|
||||
/>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-1" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
||||
|
||||
{#if $config?.features.enable_admin_export ?? true}
|
||||
<div class=" flex w-full justify-between">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { models, user } from '$lib/stores';
|
||||
import { models, settings, user, config } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -16,49 +16,69 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let config = null;
|
||||
let evaluationConfig = null;
|
||||
let showAddModel = false;
|
||||
|
||||
const submitHandler = async () => {
|
||||
config = await updateConfig(localStorage.token, config).catch((err) => {
|
||||
evaluationConfig = await updateConfig(localStorage.token, evaluationConfig).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (config) {
|
||||
if (evaluationConfig) {
|
||||
toast.success('Settings saved successfully');
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addModelHandler = async (model) => {
|
||||
config.EVALUATION_ARENA_MODELS.push(model);
|
||||
config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS.push(model);
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
|
||||
|
||||
await submitHandler();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const editModelHandler = async (model, modelIdx) => {
|
||||
config.EVALUATION_ARENA_MODELS[modelIdx] = model;
|
||||
config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS[modelIdx] = model;
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
|
||||
|
||||
await submitHandler();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const deleteModelHandler = async (modelIdx) => {
|
||||
config.EVALUATION_ARENA_MODELS = config.EVALUATION_ARENA_MODELS.filter(
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS = evaluationConfig.EVALUATION_ARENA_MODELS.filter(
|
||||
(m, mIdx) => mIdx !== modelIdx
|
||||
);
|
||||
|
||||
await submitHandler();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
config = await getConfig(localStorage.token).catch((err) => {
|
||||
evaluationConfig = await getConfig(localStorage.token).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
@@ -81,61 +101,67 @@
|
||||
}}
|
||||
>
|
||||
<div class="overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if config !== null}
|
||||
{#if evaluationConfig !== null}
|
||||
<div class="">
|
||||
<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div>
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<div class=" mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="mb-2.5 flex w-full justify-between">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
|
||||
<Switch bind:state={config.ENABLE_EVALUATION_ARENA_MODELS} />
|
||||
<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.ENABLE_EVALUATION_ARENA_MODELS}
|
||||
<hr class=" border-gray-50 dark:border-gray-700/10 my-2" />
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="text-sm font-medium">{$i18n.t('Manage Arena Models')}</div>
|
||||
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Add Arena Model')}>
|
||||
<button
|
||||
class="p-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAddModel = true;
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if (config?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
||||
{#each config.EVALUATION_ARENA_MODELS as model, index}
|
||||
<Model
|
||||
{model}
|
||||
on:edit={(e) => {
|
||||
editModelHandler(e.detail, index);
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
deleteModelHandler(index);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class=" text-center text-xs text-gray-500">
|
||||
{$i18n.t(
|
||||
`Using the default arena model with all models. Click the plus button to add custom models.`
|
||||
)}
|
||||
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
|
||||
<div>
|
||||
{$i18n.t('Manage')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Add Arena Model')}>
|
||||
<button
|
||||
class="p-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAddModel = true;
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
||||
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
|
||||
<Model
|
||||
{model}
|
||||
on:edit={(e) => {
|
||||
editModelHandler(e.detail, index);
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
deleteModelHandler(index);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class=" text-center text-xs text-gray-500">
|
||||
{$i18n.t(
|
||||
`Using the default arena model with all models. Click the plus button to add custom models.`
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Model Name')}
|
||||
@@ -260,7 +260,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={id}
|
||||
placeholder={$i18n.t('Model ID')}
|
||||
@@ -277,7 +277,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Enter description')}
|
||||
@@ -324,7 +324,7 @@
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{$models.find((model) => model.id === modelId)?.name}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -350,7 +350,7 @@
|
||||
<select
|
||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
||||
? ''
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={selectedModelId}
|
||||
>
|
||||
<option value="">{$i18n.t('Select a model')}</option>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex-shrink-0 line-clamp-1">
|
||||
<div class="shrink-0 line-clamp-1">
|
||||
{model.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||
import {
|
||||
getAdminConfig,
|
||||
getLdapConfig,
|
||||
@@ -11,7 +13,9 @@
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
|
||||
import { config, showChangelog } from '$lib/stores';
|
||||
import { compareVersion } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -19,6 +23,12 @@
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let updateAvailable = null;
|
||||
let version = {
|
||||
current: '',
|
||||
latest: ''
|
||||
};
|
||||
|
||||
let adminConfig = null;
|
||||
let webhookUrl = '';
|
||||
|
||||
@@ -39,6 +49,21 @@
|
||||
ciphers: ''
|
||||
};
|
||||
|
||||
const checkForVersionUpdates = async () => {
|
||||
updateAvailable = null;
|
||||
version = await getVersionUpdates(localStorage.token).catch((error) => {
|
||||
return {
|
||||
current: WEBUI_VERSION,
|
||||
latest: WEBUI_VERSION
|
||||
};
|
||||
});
|
||||
|
||||
console.log(version);
|
||||
|
||||
updateAvailable = compareVersion(version.latest, version.current);
|
||||
console.log(updateAvailable);
|
||||
};
|
||||
|
||||
const updateLdapServerHandler = async () => {
|
||||
if (!ENABLE_LDAP) return;
|
||||
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
|
||||
@@ -63,6 +88,8 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
checkForVersionUpdates();
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
adminConfig = await getAdminConfig(localStorage.token);
|
||||
@@ -87,381 +114,540 @@
|
||||
updateHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div class="mt-0.5 space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if adminConfig !== null}
|
||||
<div>
|
||||
<div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
<div class="">
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<div class=" flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
||||
</div>
|
||||
|
||||
<div class=" my-3 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
||||
placeholder="Select a role"
|
||||
>
|
||||
<option value="pending">{$i18n.t('pending')}</option>
|
||||
<option value="user">{$i18n.t('user')}</option>
|
||||
<option value="admin">{$i18n.t('admin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between pr-2 my-3">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_API_KEY} />
|
||||
</div>
|
||||
|
||||
{#if adminConfig?.ENABLE_API_KEY}
|
||||
<div class=" flex w-full justify-between pr-2 my-3">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('API Key Endpoint Restrictions')}
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
|
||||
<div>
|
||||
{$i18n.t('Version')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||
<div class="flex gap-1">
|
||||
<Tooltip content={WEBUI_BUILD_HASH}>
|
||||
v{WEBUI_VERSION}
|
||||
</Tooltip>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
|
||||
<div class=" flex w-full flex-col pr-2">
|
||||
<div class=" text-xs font-medium">
|
||||
{$i18n.t('Allowed Endpoints')}
|
||||
<button
|
||||
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showChangelog.set(true);
|
||||
}}
|
||||
>
|
||||
<div>{$i18n.t("See what's new")}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
|
||||
bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
|
||||
/>
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
|
||||
<a
|
||||
href="https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints"
|
||||
target="_blank"
|
||||
class=" text-gray-300 font-medium underline"
|
||||
>
|
||||
{$i18n.t('To learn more about available endpoints, visit our documentation.')}
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="text-xs pr-2">
|
||||
<div class="">
|
||||
{$i18n.t('Help')}
|
||||
</div>
|
||||
<div class=" text-xs text-gray-500">
|
||||
{$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="flex-shrink-0 text-xs font-medium underline"
|
||||
href="https://docs.openwebui.com/"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Documentation')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
<img
|
||||
alt="Discord"
|
||||
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) "http://localhost:3000"`}
|
||||
bind:value={adminConfig.WEBUI_URL}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Valid time units:')}
|
||||
<span class=" text-gray-300 font-medium"
|
||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`https://example.com/webhook`}
|
||||
bind:value={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="pt-1 flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Channels')} ({$i18n.t('Beta')})
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
||||
|
||||
<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">{$i18n.t('LDAP')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_LDAP}
|
||||
on:change={async () => {
|
||||
updateLdapConfig(localStorage.token, ENABLE_LDAP);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_LDAP}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Label')}
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="text-xs pr-2">
|
||||
<div class="">
|
||||
{$i18n.t('License')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter server label')}
|
||||
bind:value={LDAP_SERVER.label}
|
||||
/>
|
||||
|
||||
{#if $config?.license_metadata}
|
||||
<a
|
||||
href="https://docs.openwebui.com/enterprise"
|
||||
target="_blank"
|
||||
class="text-gray-500 mt-0.5"
|
||||
>
|
||||
<span class=" capitalize text-black dark:text-white"
|
||||
>{$config?.license_metadata?.type}
|
||||
license</span
|
||||
>
|
||||
registered to
|
||||
<span class=" capitalize text-black dark:text-white"
|
||||
>{$config?.license_metadata?.organization_name}</span
|
||||
>
|
||||
for
|
||||
<span class=" font-medium text-black dark:text-white"
|
||||
>{$config?.license_metadata?.seats ?? 'Unlimited'} users.</span
|
||||
>
|
||||
</a>
|
||||
{#if $config?.license_metadata?.html}
|
||||
<div class="mt-0.5">
|
||||
{@html DOMPurify.sanitize($config?.license_metadata?.html)}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<a
|
||||
class=" text-xs hover:underline"
|
||||
href="https://docs.openwebui.com/enterprise"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="text-gray-500">
|
||||
{$i18n.t(
|
||||
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Host')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter server host')}
|
||||
bind:value={LDAP_SERVER.host}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Port')}
|
||||
</div>
|
||||
<Tooltip
|
||||
placement="top-start"
|
||||
content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
|
||||
className="w-full"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter server port')}
|
||||
bind:value={LDAP_SERVER.port}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Application DN')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('The Application Account DN you bind with for search')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter Application DN')}
|
||||
bind:value={LDAP_SERVER.app_dn}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Application DN Password')}
|
||||
</div>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Application DN Password')}
|
||||
bind:value={LDAP_SERVER.app_dn_password}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Attribute for Mail')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The LDAP attribute that maps to the mail that users use to sign in.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Example: mail')}
|
||||
bind:value={LDAP_SERVER.attribute_for_mail}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Attribute for Username')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The LDAP attribute that maps to the username that users use to sign in.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Example: sAMAccountName or uid or userPrincipalName')}
|
||||
bind:value={LDAP_SERVER.attribute_for_username}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Search Base')}
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('The base to search for users')} placement="top-start">
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
|
||||
bind:value={LDAP_SERVER.search_base}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Search Filters')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
|
||||
bind:value={LDAP_SERVER.search_filters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://ldap.com/ldap-filters/"
|
||||
target="_blank"
|
||||
|
||||
<!-- <button
|
||||
class="flex-shrink-0 text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
>
|
||||
{$i18n.t('Click here for filter guides.')}
|
||||
</a>
|
||||
{$i18n.t('Activate')}
|
||||
</button> -->
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
||||
placeholder="Select a role"
|
||||
>
|
||||
<option value="pending">{$i18n.t('pending')}</option>
|
||||
<option value="user">{$i18n.t('user')}</option>
|
||||
<option value="admin">{$i18n.t('admin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_API_KEY} />
|
||||
</div>
|
||||
|
||||
{#if adminConfig?.ENABLE_API_KEY}
|
||||
<div class="mb-2.5 flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('API Key Endpoint Restrictions')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
|
||||
</div>
|
||||
|
||||
{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
|
||||
<div class=" flex w-full flex-col pr-2">
|
||||
<div class=" text-xs font-medium">
|
||||
{$i18n.t('Allowed Endpoints')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
|
||||
bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
|
||||
/>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
|
||||
<a
|
||||
href="https://docs.openwebui.com/getting-started/api-endpoints"
|
||||
target="_blank"
|
||||
class=" text-gray-300 font-medium underline"
|
||||
>
|
||||
{$i18n.t('To learn more about available endpoints, visit our documentation.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class=" mb-2.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Valid time units:')}
|
||||
<span class=" text-gray-300 font-medium"
|
||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">{$i18n.t('TLS')}</div>
|
||||
<div class=" font-medium">{$i18n.t('LDAP')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch bind:state={LDAP_SERVER.use_tls} />
|
||||
<Switch
|
||||
bind:state={ENABLE_LDAP}
|
||||
on:change={async () => {
|
||||
updateLdapConfig(localStorage.token, ENABLE_LDAP);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if LDAP_SERVER.use_tls}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
|
||||
{$i18n.t('Certificate Path')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter certificate path')}
|
||||
bind:value={LDAP_SERVER.certificate_path}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Ciphers')}
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
|
||||
|
||||
{#if ENABLE_LDAP}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Label')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-none py-0.5"
|
||||
placeholder={$i18n.t('Example: ALL')}
|
||||
bind:value={LDAP_SERVER.ciphers}
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter server label')}
|
||||
bind:value={LDAP_SERVER.label}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Host')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter server host')}
|
||||
bind:value={LDAP_SERVER.host}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Port')}
|
||||
</div>
|
||||
<Tooltip
|
||||
placement="top-start"
|
||||
content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
|
||||
className="w-full"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter server port')}
|
||||
bind:value={LDAP_SERVER.port}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Application DN')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('The Application Account DN you bind with for search')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter Application DN')}
|
||||
bind:value={LDAP_SERVER.app_dn}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Application DN Password')}
|
||||
</div>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Application DN Password')}
|
||||
bind:value={LDAP_SERVER.app_dn_password}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Attribute for Mail')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The LDAP attribute that maps to the mail that users use to sign in.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Example: mail')}
|
||||
bind:value={LDAP_SERVER.attribute_for_mail}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Attribute for Username')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The LDAP attribute that maps to the username that users use to sign in.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t(
|
||||
'Example: sAMAccountName or uid or userPrincipalName'
|
||||
)}
|
||||
bind:value={LDAP_SERVER.attribute_for_username}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Search Base')}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('The base to search for users')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
|
||||
bind:value={LDAP_SERVER.search_base}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Search Filters')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
|
||||
bind:value={LDAP_SERVER.search_filters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://ldap.com/ldap-filters/"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for filter guides.')}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('TLS')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch bind:state={LDAP_SERVER.use_tls} />
|
||||
</div>
|
||||
</div>
|
||||
{#if LDAP_SERVER.use_tls}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
|
||||
{$i18n.t('Certificate Path')}
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
required
|
||||
placeholder={$i18n.t('Enter certificate path')}
|
||||
bind:value={LDAP_SERVER.certificate_path}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Ciphers')}
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden py-0.5"
|
||||
placeholder={$i18n.t('Example: ALL')}
|
||||
bind:value={LDAP_SERVER.ciphers}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Enable Community Sharing')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Channels')} ({$i18n.t('Beta')})
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={`e.g.) "http://localhost:3000"`}
|
||||
bind:value={adminConfig.WEBUI_URL}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={`https://example.com/webhook`}
|
||||
bind:value={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
||||
@@ -261,6 +261,9 @@
|
||||
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
|
||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||
config.enabled = false;
|
||||
} else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') {
|
||||
toast.error($i18n.t('Gemini API Key is required.'));
|
||||
config.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +287,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={config.engine}
|
||||
placeholder={$i18n.t('Select Engine')}
|
||||
on:change={async () => {
|
||||
@@ -294,11 +297,12 @@
|
||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||
<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
|
||||
<option value="gemini">{$i18n.t('Gemini')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
|
||||
@@ -307,7 +311,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
|
||||
/>
|
||||
@@ -386,7 +390,7 @@
|
||||
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
|
||||
<input
|
||||
list="sampler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
|
||||
/>
|
||||
@@ -408,7 +412,7 @@
|
||||
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
|
||||
<input
|
||||
list="scheduler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
|
||||
/>
|
||||
@@ -429,7 +433,7 @@
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
|
||||
/>
|
||||
@@ -443,7 +447,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={config.comfyui.COMFYUI_BASE_URL}
|
||||
/>
|
||||
@@ -497,7 +501,7 @@
|
||||
|
||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||
<textarea
|
||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
|
||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
||||
rows="10"
|
||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||
required
|
||||
@@ -525,7 +529,7 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||
@@ -548,7 +552,7 @@
|
||||
<div class="text-xs flex flex-col gap-1.5">
|
||||
{#each requiredWorkflowNodes as node}
|
||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
>
|
||||
@@ -558,7 +562,7 @@
|
||||
<div class="">
|
||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||
<input
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
|
||||
placeholder="Key"
|
||||
bind:value={node.key}
|
||||
required
|
||||
@@ -572,7 +576,7 @@
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
|
||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
|
||||
placeholder="Node Ids"
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
@@ -593,7 +597,7 @@
|
||||
|
||||
<div class="flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full text-sm bg-transparent outline-none"
|
||||
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={config.openai.OPENAI_API_BASE_URL}
|
||||
required
|
||||
@@ -605,11 +609,29 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if config?.engine === 'gemini'}
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
|
||||
|
||||
<div class="flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full text-sm bg-transparent outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={config.gemini.GEMINI_API_BASE_URL}
|
||||
required
|
||||
/>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={config.gemini.GEMINI_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if config?.enabled}
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||
@@ -620,7 +642,7 @@
|
||||
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={imageGenerationConfig.MODEL}
|
||||
placeholder="Select a model"
|
||||
required
|
||||
@@ -644,7 +666,7 @@
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||
bind:value={imageGenerationConfig.IMAGE_SIZE}
|
||||
required
|
||||
@@ -660,7 +682,7 @@
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||
bind:value={imageGenerationConfig.IMAGE_STEPS}
|
||||
required
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
let taskConfig = {
|
||||
TASK_MODEL: '',
|
||||
TASK_MODEL_EXTERNAL: '',
|
||||
ENABLE_TITLE_GENERATION: true,
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
|
||||
ENABLE_AUTOCOMPLETE_GENERATION: true,
|
||||
@@ -31,7 +32,8 @@
|
||||
ENABLE_TAGS_GENERATION: true,
|
||||
ENABLE_SEARCH_QUERY_GENERATION: true,
|
||||
ENABLE_RETRIEVAL_QUERY_GENERATION: true,
|
||||
QUERY_GENERATION_PROMPT_TEMPLATE: ''
|
||||
QUERY_GENERATION_PROMPT_TEMPLATE: '',
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: ''
|
||||
};
|
||||
|
||||
let promptSuggestions = [];
|
||||
@@ -49,7 +51,7 @@
|
||||
onMount(async () => {
|
||||
taskConfig = await getTaskConfig(localStorage.token);
|
||||
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
promptSuggestions = $config?.default_prompt_suggestions ?? [];
|
||||
banners = await getBanners(localStorage.token);
|
||||
});
|
||||
|
||||
@@ -67,9 +69,13 @@
|
||||
}}
|
||||
>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex items-center">
|
||||
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" mb-1 font-medium flex items-center">
|
||||
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
||||
@@ -91,11 +97,12 @@
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
|
||||
<div class=" mb-2.5 flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={taskConfig.TASK_MODEL}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
@@ -111,7 +118,7 @@
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
@@ -125,72 +132,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between">
|
||||
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Autocomplete Generation')}
|
||||
{$i18n.t('Title Generation')}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
|
||||
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
|
||||
</Tooltip>
|
||||
<Switch bind:state={taskConfig.ENABLE_TITLE_GENERATION} />
|
||||
</div>
|
||||
|
||||
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-xs font-medium">
|
||||
{$i18n.t('Autocomplete Generation Input Max Length')}
|
||||
</div>
|
||||
{#if taskConfig.ENABLE_TITLE_GENERATION}
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Character limit for autocomplete generation input')}
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full outline-none bg-transparent"
|
||||
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
|
||||
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
|
||||
<Textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between">
|
||||
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Tags Generation')}
|
||||
</div>
|
||||
@@ -199,8 +167,8 @@
|
||||
</div>
|
||||
|
||||
{#if taskConfig.ENABLE_TAGS_GENERATION}
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
@@ -216,9 +184,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between">
|
||||
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Retrieval Query Generation')}
|
||||
</div>
|
||||
@@ -226,7 +192,7 @@
|
||||
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between">
|
||||
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Web Search Query Generation')}
|
||||
</div>
|
||||
@@ -234,8 +200,8 @@
|
||||
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
@@ -249,117 +215,96 @@
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
|
||||
|
||||
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Banners')}
|
||||
<div class="mb-2.5 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Autocomplete Generation')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
|
||||
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each banners as banner, bannerIdx}
|
||||
<div class=" flex justify-between">
|
||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
||||
bind:value={banner.type}
|
||||
required
|
||||
>
|
||||
{#if banner.type == ''}
|
||||
<option value="" selected disabled class="text-gray-900"
|
||||
>{$i18n.t('Type')}</option
|
||||
>
|
||||
{/if}
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
/>
|
||||
|
||||
<div class="relative top-1.5 -left-2">
|
||||
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<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>
|
||||
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">
|
||||
{$i18n.t('Autocomplete Generation Input Max Length')}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Character limit for autocomplete generation input')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full outline-hidden bg-transparent"
|
||||
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
|
||||
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<div class=" space-y-3">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" {banners.length > 0 ? ' mb-3' : ''}">
|
||||
<div class="mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -375,40 +320,48 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||
{#each promptSuggestions as prompt, promptIdx}
|
||||
<div
|
||||
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
||||
>
|
||||
<div class="flex flex-col flex-1 pl-1">
|
||||
<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||
bind:value={prompt.title[0]}
|
||||
/>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||
bind:value={prompt.title[1]}
|
||||
/>
|
||||
</div>
|
||||
<div class=" flex flex-col space-y-1">
|
||||
{#each banners as banner, bannerIdx}
|
||||
<div class=" flex justify-between">
|
||||
<div
|
||||
class="flex flex-row flex-1 border rounded-xl border-gray-100 dark:border-gray-850"
|
||||
>
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-hidden"
|
||||
bind:value={banner.type}
|
||||
required
|
||||
>
|
||||
{#if banner.type == ''}
|
||||
<option value="" selected disabled class="text-gray-900"
|
||||
>{$i18n.t('Type')}</option
|
||||
>
|
||||
{/if}
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||
rows="3"
|
||||
bind:value={prompt.content}
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
/>
|
||||
|
||||
<div class="relative top-1.5 -left-2">
|
||||
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-3"
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
promptSuggestions.splice(promptIdx, 1);
|
||||
promptSuggestions = promptSuggestions;
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -425,14 +378,97 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<div class=" space-y-3">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||
{#each promptSuggestions as prompt, promptIdx}
|
||||
<div
|
||||
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
|
||||
>
|
||||
<div class="flex flex-col flex-1 pl-1">
|
||||
<div class="flex border-b border-gray-100 dark:border-gray-850 w-full">
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
|
||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||
bind:value={prompt.title[0]}
|
||||
/>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
|
||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||
bind:value={prompt.title[1]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850 resize-none"
|
||||
placeholder={$i18n.t(
|
||||
'Prompt (e.g. Tell me a fun fact about the Roman Empire)'
|
||||
)}
|
||||
rows="3"
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-3"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
promptSuggestions.splice(promptIdx, 1);
|
||||
promptSuggestions = promptSuggestions;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
const init = async () => {
|
||||
workspaceModels = await getBaseModels(localStorage.token);
|
||||
baseModels = await getModels(localStorage.token, true);
|
||||
baseModels = await getModels(localStorage.token, null, true);
|
||||
|
||||
models = baseModels.map((m) => {
|
||||
const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
|
||||
@@ -111,7 +111,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
_models.set(await getModels(localStorage.token));
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
await init();
|
||||
};
|
||||
|
||||
@@ -133,7 +138,12 @@
|
||||
}
|
||||
|
||||
// await init();
|
||||
_models.set(await getModels(localStorage.token));
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
@@ -189,7 +199,7 @@
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={searchValue}
|
||||
placeholder={$i18n.t('Search Models')}
|
||||
/>
|
||||
@@ -330,7 +340,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
await _models.set(await getModels(localStorage.token));
|
||||
await _models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
init();
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let initHandler = () => {};
|
||||
@@ -26,6 +28,9 @@
|
||||
let defaultModelIds = [];
|
||||
let modelIds = [];
|
||||
|
||||
let sortKey = '';
|
||||
let sortOrder = '';
|
||||
|
||||
let loading = false;
|
||||
let showResetModal = false;
|
||||
|
||||
@@ -71,6 +76,9 @@
|
||||
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
|
||||
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
||||
];
|
||||
|
||||
sortKey = '';
|
||||
sortOrder = '';
|
||||
};
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
@@ -145,9 +153,45 @@
|
||||
>
|
||||
<div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="mb-1 flex justify-between">
|
||||
<button
|
||||
class="mb-1 flex gap-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
sortKey = 'model';
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
sortOrder = 'desc';
|
||||
} else {
|
||||
sortOrder = 'asc';
|
||||
}
|
||||
|
||||
modelIds = modelIds
|
||||
.filter((id) => id !== '')
|
||||
.sort((a, b) => {
|
||||
const nameA = $models.find((model) => model.id === a)?.name || a;
|
||||
const nameB = $models.find((model) => model.id === b)?.name || b;
|
||||
return sortOrder === 'desc'
|
||||
? nameA.localeCompare(nameB)
|
||||
: nameB.localeCompare(nameA);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
|
||||
</div>
|
||||
|
||||
{#if sortKey === 'model'}
|
||||
<span class="font-normal self-center">
|
||||
{#if sortOrder === 'asc'}
|
||||
<ChevronUp className="size-3" />
|
||||
{:else}
|
||||
<ChevronDown className="size-3" />
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible">
|
||||
<ChevronUp className="size-3" />
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<ModelList bind:modelIds />
|
||||
</div>
|
||||
@@ -165,7 +209,7 @@
|
||||
<select
|
||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
||||
? ''
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={selectedModelId}
|
||||
>
|
||||
<option value="">{$i18n.t('Select a model')}</option>
|
||||
@@ -186,7 +230,7 @@
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{$models.find((model) => model.id === modelId)?.name}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{#if ollamaConfig}
|
||||
<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
|
||||
<select
|
||||
class="w-full py-2 px-4 text-sm outline-none bg-transparent"
|
||||
class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
|
||||
bind:value={selectedUrlIdx}
|
||||
placeholder={$i18n.t('Select an Ollama instance')}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
|
||||
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config, settings } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
|
||||
import {
|
||||
@@ -235,7 +235,12 @@
|
||||
})
|
||||
);
|
||||
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
toast.error($i18n.t('Download canceled'));
|
||||
}
|
||||
@@ -394,7 +399,12 @@
|
||||
modelTransferring = false;
|
||||
uploadProgress = null;
|
||||
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const deleteModelHandler = async () => {
|
||||
@@ -407,7 +417,12 @@
|
||||
}
|
||||
|
||||
deleteModelTag = '';
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const cancelModelPullHandler = async (model: string) => {
|
||||
@@ -506,7 +521,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
|
||||
createModelLoading = false;
|
||||
|
||||
@@ -578,7 +598,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||
modelTag: 'mistral:7b'
|
||||
})}
|
||||
@@ -720,7 +740,7 @@
|
||||
class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
|
||||
>
|
||||
<select
|
||||
class="w-full py-2 px-4 text-sm outline-none bg-transparent"
|
||||
class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
|
||||
bind:value={deleteModelTag}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
@@ -761,7 +781,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2 flex flex-col gap-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||
modelTag: 'my-modelfile'
|
||||
})}
|
||||
@@ -771,7 +791,7 @@
|
||||
|
||||
<textarea
|
||||
bind:value={createModelObject}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none scrollbar-hidden"
|
||||
rows="6"
|
||||
placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
|
||||
disabled={createModelLoading}
|
||||
@@ -850,7 +870,7 @@
|
||||
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
if (modelUploadMode === 'file') {
|
||||
modelUploadMode = 'url';
|
||||
@@ -902,7 +922,7 @@
|
||||
{:else}
|
||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||
<input
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden {modelFileUrl !==
|
||||
''
|
||||
? 'mr-2'
|
||||
: ''}"
|
||||
@@ -978,7 +998,7 @@
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={modelFileContent}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -21,14 +21,24 @@
|
||||
modelIds = modelList;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
sortable = Sortable.create(modelListElement, {
|
||||
animation: 150,
|
||||
onUpdate: async (event) => {
|
||||
positionChangeHandler();
|
||||
}
|
||||
});
|
||||
});
|
||||
$: if (modelIds) {
|
||||
init();
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
if (sortable) {
|
||||
sortable.destroy();
|
||||
}
|
||||
|
||||
if (modelListElement) {
|
||||
sortable = Sortable.create(modelListElement, {
|
||||
animation: 150,
|
||||
onUpdate: async (event) => {
|
||||
positionChangeHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if modelIds.length > 0}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { models } from '$lib/stores';
|
||||
import { config, models, settings } from '$lib/stores';
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
@@ -63,7 +63,12 @@
|
||||
if (res) {
|
||||
toast.success($i18n.t('Valves updated successfully'));
|
||||
setPipelines();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
saveHandler();
|
||||
}
|
||||
} else {
|
||||
@@ -125,7 +130,12 @@
|
||||
if (res) {
|
||||
toast.success($i18n.t('Pipeline downloaded successfully'));
|
||||
setPipelines();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
downloading = false;
|
||||
@@ -150,7 +160,12 @@
|
||||
if (res) {
|
||||
toast.success($i18n.t('Pipeline downloaded successfully'));
|
||||
setPipelines();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toast.error($i18n.t('No file selected'));
|
||||
@@ -179,7 +194,12 @@
|
||||
if (res) {
|
||||
toast.success($i18n.t('Pipeline deleted successfully'));
|
||||
setPipelines();
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,7 +234,7 @@
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={selectedPipelinesUrlIdx}
|
||||
placeholder={$i18n.t('Select a pipeline url')}
|
||||
on:change={async () => {
|
||||
@@ -251,7 +271,7 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('pipelines-upload-input')?.click();
|
||||
@@ -328,7 +348,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Enter Github Raw URL')}
|
||||
bind:value={pipelineDownloadUrl}
|
||||
/>
|
||||
@@ -398,7 +418,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
|
||||
|
||||
{#if pipelines !== null}
|
||||
{#if pipelines.length > 0}
|
||||
@@ -412,7 +432,7 @@
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={selectedPipelineIdx}
|
||||
placeholder={$i18n.t('Select a pipeline')}
|
||||
on:change={async () => {
|
||||
@@ -462,7 +482,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
valves[property] = (valves[property] ?? null) === null ? '' : null;
|
||||
@@ -482,7 +502,7 @@
|
||||
<div class=" flex-1">
|
||||
{#if valves_spec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={valves[property]}
|
||||
>
|
||||
{#each valves_spec.properties[property].enum as option}
|
||||
@@ -503,7 +523,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={valves_spec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -18,14 +19,18 @@
|
||||
'brave',
|
||||
'kagi',
|
||||
'mojeek',
|
||||
'bocha',
|
||||
'serpstack',
|
||||
'serper',
|
||||
'serply',
|
||||
'searchapi',
|
||||
'serpapi',
|
||||
'duckduckgo',
|
||||
'tavily',
|
||||
'jina',
|
||||
'bing'
|
||||
'bing',
|
||||
'exa',
|
||||
'perplexity'
|
||||
];
|
||||
|
||||
let youtubeLanguage = 'en';
|
||||
@@ -33,6 +38,16 @@
|
||||
let youtubeProxyUrl = '';
|
||||
|
||||
const submitHandler = async () => {
|
||||
// Convert domain filter string to array before sending
|
||||
if (webConfig.search.domain_filter_list) {
|
||||
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list
|
||||
.split(',')
|
||||
.map((domain) => domain.trim())
|
||||
.filter((domain) => domain.length > 0);
|
||||
} else {
|
||||
webConfig.search.domain_filter_list = [];
|
||||
}
|
||||
|
||||
const res = await updateRAGConfig(localStorage.token, {
|
||||
web: webConfig,
|
||||
youtube: {
|
||||
@@ -41,6 +56,8 @@
|
||||
proxy_url: youtubeProxyUrl
|
||||
}
|
||||
});
|
||||
|
||||
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
@@ -48,6 +65,10 @@
|
||||
|
||||
if (res) {
|
||||
webConfig = res.web;
|
||||
// Convert array back to comma-separated string for display
|
||||
if (webConfig?.search?.domain_filter_list) {
|
||||
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
|
||||
}
|
||||
|
||||
youtubeLanguage = res.youtube.language.join(',');
|
||||
youtubeTranslation = res.youtube.translation;
|
||||
@@ -65,306 +86,427 @@
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if webConfig}
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">
|
||||
{$i18n.t('Web Search')}
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Enable Web Search')}
|
||||
{$i18n.t('Web Search')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<Switch bind:state={webConfig.search.enabled} />
|
||||
</div>
|
||||
|
||||
<Switch bind:state={webConfig.search.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={webConfig.search.engine}
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each webSearchEngines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Web Search Engine')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={webConfig.search.engine}
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each webSearchEngines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if webConfig.search.engine !== ''}
|
||||
<div class="mt-1.5">
|
||||
{#if webConfig.search.engine !== ''}
|
||||
{#if webConfig.search.engine === 'searxng'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Searxng Query URL')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Searxng Query URL')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||
bind:value={webConfig.search.searxng_query_url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||
bind:value={webConfig.search.searxng_query_url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'google_pse'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Google PSE API Key')}
|
||||
bind:value={webConfig.search.google_pse_api_key}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE Engine Id')}
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Google PSE API Key')}
|
||||
bind:value={webConfig.search.google_pse_api_key}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE Engine Id')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||
bind:value={webConfig.search.google_pse_engine_id}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||
bind:value={webConfig.search.google_pse_engine_id}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'brave'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Brave Search API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Brave Search API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Brave Search API Key')}
|
||||
bind:value={webConfig.search.brave_search_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Brave Search API Key')}
|
||||
bind:value={webConfig.search.brave_search_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'kagi'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Kagi Search API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Kagi Search API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Kagi Search API Key')}
|
||||
bind:value={webConfig.search.kagi_search_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Kagi Search API Key')}
|
||||
bind:value={webConfig.search.kagi_search_api_key}
|
||||
/>
|
||||
</div>
|
||||
.
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'mojeek'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Mojeek Search API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Mojeek Search API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Mojeek Search API Key')}
|
||||
bind:value={webConfig.search.mojeek_search_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Mojeek Search API Key')}
|
||||
bind:value={webConfig.search.mojeek_search_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'bocha'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Bocha Search API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Bocha Search API Key')}
|
||||
bind:value={webConfig.search.bocha_search_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serpstack'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serpstack API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serpstack API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serpstack API Key')}
|
||||
bind:value={webConfig.search.serpstack_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serpstack API Key')}
|
||||
bind:value={webConfig.search.serpstack_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serper'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serper API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serper API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serper API Key')}
|
||||
bind:value={webConfig.search.serper_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serper API Key')}
|
||||
bind:value={webConfig.search.serper_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serply'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serply API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serply API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serply API Key')}
|
||||
bind:value={webConfig.search.serply_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Serply API Key')}
|
||||
bind:value={webConfig.search.serply_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'searchapi'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SearchApi API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SearchApi API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter SearchApi API Key')}
|
||||
bind:value={webConfig.search.searchapi_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter SearchApi API Key')}
|
||||
bind:value={webConfig.search.searchapi_api_key}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SearchApi Engine')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter SearchApi Engine')}
|
||||
bind:value={webConfig.search.searchapi_engine}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SearchApi Engine')}
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serpapi'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SerpApi API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter SearchApi Engine')}
|
||||
bind:value={webConfig.search.searchapi_engine}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter SerpApi API Key')}
|
||||
bind:value={webConfig.search.serpapi_api_key}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('SerpApi Engine')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter SerpApi Engine')}
|
||||
bind:value={webConfig.search.serpapi_engine}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'tavily'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Tavily API Key')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Tavily API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Tavily API Key')}
|
||||
bind:value={webConfig.search.tavily_api_key}
|
||||
/>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Tavily API Key')}
|
||||
bind:value={webConfig.search.tavily_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'jina'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Jina API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Jina API Key')}
|
||||
bind:value={webConfig.search.jina_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'exa'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Exa API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Exa API Key')}
|
||||
bind:value={webConfig.search.exa_api_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'perplexity'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Jina API Key')}
|
||||
{$i18n.t('Perplexity API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Jina API Key')}
|
||||
bind:value={webConfig.search.jina_api_key}
|
||||
placeholder={$i18n.t('Enter Perplexity API Key')}
|
||||
bind:value={webConfig.search.perplexity_api_key}
|
||||
/>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'bing'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Bing Search V7 Endpoint')}
|
||||
</div>
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Bing Search V7 Endpoint')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
||||
bind:value={webConfig.search.bing_search_v7_endpoint}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
||||
bind:value={webConfig.search.bing_search_v7_endpoint}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Bing Search V7 Subscription Key')}
|
||||
<div class="mt-2">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Bing Search V7 Subscription Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
|
||||
bind:value={webConfig.search.bing_search_v7_subscription_key}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
|
||||
bind:value={webConfig.search.bing_search_v7_subscription_key}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if webConfig.search.enabled}
|
||||
<div class="mt-2 flex gap-2 mb-1">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Search Result Count')}
|
||||
{#if webConfig.search.enabled}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Search Result Count')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Search Result Count')}
|
||||
bind:value={webConfig.search.result_count}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Concurrent Requests')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t('Concurrent Requests')}
|
||||
bind:value={webConfig.search.concurrent_requests}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div class=" text-xs font-medium mb-1">
|
||||
{$i18n.t('Domain Filter List')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Search Result Count')}
|
||||
bind:value={webConfig.search.result_count}
|
||||
required
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
placeholder={$i18n.t(
|
||||
'Enter domains separated by commas (e.g., example.com,site.org)'
|
||||
)}
|
||||
bind:value={webConfig.search.domain_filter_list}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Concurrent Requests')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Concurrent Requests')}
|
||||
bind:value={webConfig.search.concurrent_requests}
|
||||
required
|
||||
/>
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
<Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
|
||||
{$i18n.t('Bypass Embedding and Retrieval')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<Tooltip
|
||||
content={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||
>
|
||||
<Switch bind:state={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">
|
||||
{$i18n.t('Web Loader Settings')}
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Trust Proxy Environment')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<Tooltip
|
||||
content={webConfig.search.trust_env
|
||||
? 'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents'
|
||||
: 'Use no proxy to fetch page contents.'}
|
||||
>
|
||||
<Switch bind:state={webConfig.search.trust_env} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class="mb-3">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Bypass SSL verification for Websites')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification;
|
||||
submitHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if webConfig.web_loader_ssl_verification === false}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<div class="flex items-center relative">
|
||||
<Switch bind:state={webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 mb-1 text-sm font-medium">
|
||||
{$i18n.t('Youtube Loader Settings')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Youtube Language')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter language codes')}
|
||||
bind:value={youtubeLanguage}
|
||||
@@ -372,14 +514,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<div class=" mb-2.5 flex flex-col w-full justify-between">
|
||||
<div class=" mb-1 text-xs font-medium">
|
||||
{$i18n.t('Youtube Proxy URL')}
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
||||
bind:value={youtubeProxyUrl}
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
},
|
||||
features: {
|
||||
web_search: true,
|
||||
image_generation: true
|
||||
image_generation: true,
|
||||
code_interpreter: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,7 +140,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={search}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
@@ -194,7 +195,7 @@
|
||||
<div class="w-full"></div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-1.5 border-gray-50 dark:border-gray-850" />
|
||||
<hr class="mt-1.5 border-gray-100 dark:border-gray-850" />
|
||||
|
||||
{#each filteredGroups as group}
|
||||
<div class="my-2">
|
||||
@@ -204,7 +205,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="mb-2 border-gray-50 dark:border-gray-850" />
|
||||
<hr class="mb-2 border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<GroupModal
|
||||
bind:show={showDefaultPermissionsModal}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Group Name')}
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<Textarea
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
|
||||
rows={2}
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Group Description')}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Group Name')}
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="text-gray-500">#</div>
|
||||
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
bind:value={color}
|
||||
placeholder={$i18n.t('Hex Color')}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<Textarea
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
|
||||
rows={4}
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Group Description')}
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
},
|
||||
features: {
|
||||
web_search: true,
|
||||
image_generation: true
|
||||
image_generation: true,
|
||||
code_interpreter: true
|
||||
}
|
||||
};
|
||||
export let userIds = [];
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
features: {
|
||||
web_search: true,
|
||||
image_generation: true
|
||||
image_generation: true,
|
||||
code_interpreter: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,7 +76,7 @@
|
||||
<div class=" text-sm flex-1 rounded-lg">
|
||||
{modelId}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -101,7 +102,7 @@
|
||||
<select
|
||||
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
|
||||
? ''
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={selectedModelId}
|
||||
>
|
||||
<option value="">{$i18n.t('Select a model')}</option>
|
||||
@@ -136,7 +137,7 @@
|
||||
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full bg-transparent outline-none py-0.5 text-sm"
|
||||
class="w-full bg-transparent outline-hidden py-0.5 text-sm"
|
||||
bind:value={permissions.model.default_id}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
@@ -149,7 +150,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> -->
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> -->
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
||||
@@ -191,7 +192,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||
@@ -237,7 +238,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
|
||||
@@ -257,5 +258,13 @@
|
||||
|
||||
<Switch bind:state={permissions.features.image_generation} />
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Code Interpreter')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={permissions.features.code_interpreter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -26,6 +28,7 @@
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import About from '$lib/components/chat/Settings/About.svelte';
|
||||
import Banner from '$lib/components/common/Banner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -83,8 +86,9 @@
|
||||
return true;
|
||||
} else {
|
||||
let name = user.name.toLowerCase();
|
||||
let email = user.email.toLowerCase();
|
||||
const query = search.toLowerCase();
|
||||
return name.includes(query);
|
||||
return name.includes(query) || email.includes(query);
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
@@ -121,12 +125,43 @@
|
||||
/>
|
||||
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null && users.length > $config?.license_metadata?.seats}
|
||||
<div class=" mt-1 mb-2 text-xs text-red-500">
|
||||
<Banner
|
||||
className="mx-0"
|
||||
banner={{
|
||||
type: 'error',
|
||||
title: 'License Error',
|
||||
content:
|
||||
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.',
|
||||
dismissable: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Users')}
|
||||
<div class="flex-shrink-0">
|
||||
{$i18n.t('Users')}
|
||||
</div>
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null}
|
||||
{#if users.length > $config?.license_metadata?.seats}
|
||||
<span class="text-lg font-medium text-red-500"
|
||||
>{users.length} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">available users</span></span
|
||||
>
|
||||
{:else}
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||
>{users.length} of {$config?.license_metadata?.seats}
|
||||
<span class="text-sm font-normal">available users</span></span
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
@@ -147,7 +182,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={search}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
@@ -169,9 +204,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
|
||||
<div
|
||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
||||
>
|
||||
<table
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
|
||||
>
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
||||
@@ -364,7 +401,7 @@
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1">
|
||||
{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(user.created_at * 1000).format('LL')}
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-none"
|
||||
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-hidden"
|
||||
bind:value={_user.role}
|
||||
placeholder={$i18n.t('Enter Your Role')}
|
||||
required
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||
type="text"
|
||||
bind:value={_user.name}
|
||||
placeholder={$i18n.t('Enter Your Full Name')}
|
||||
@@ -208,14 +208,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2.5 w-full" />
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||
type="email"
|
||||
bind:value={_user.email}
|
||||
placeholder={$i18n.t('Enter Your Email')}
|
||||
@@ -229,7 +229,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||
type="password"
|
||||
bind:value={_user.password}
|
||||
placeholder={$i18n.t('Enter Your Password')}
|
||||
@@ -249,7 +249,7 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-user-csv-input')?.click();
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
import { updateUserById } from '$lib/apis/users';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let show = false;
|
||||
export let selectedUser;
|
||||
@@ -63,7 +65,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-800" />
|
||||
<hr class="border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full p-5 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">
|
||||
@@ -87,12 +89,12 @@
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Created at')}
|
||||
{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
|
||||
|
||||
<div class=" flex flex-col space-y-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
@@ -100,7 +102,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||
type="email"
|
||||
bind:value={_user.email}
|
||||
autocomplete="off"
|
||||
@@ -115,7 +117,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
|
||||
type="text"
|
||||
bind:value={_user.name}
|
||||
autocomplete="off"
|
||||
@@ -129,7 +131,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
|
||||
type="password"
|
||||
bind:value={_user.password}
|
||||
autocomplete="new-password"
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||
|
||||
@@ -80,7 +82,7 @@
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
|
||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-850"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
@@ -130,7 +132,7 @@
|
||||
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem] justify-end">
|
||||
<div class="my-auto shrink-0">
|
||||
{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
|
||||
{dayjs(chat.updated_at * 1000).format('LLL')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
<PaneResizer
|
||||
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
|
||||
>
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||
</div>
|
||||
</PaneResizer>
|
||||
|
||||
@@ -103,7 +103,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = async (event) => {
|
||||
@@ -155,22 +157,6 @@
|
||||
}
|
||||
|
||||
files = [...files, fileItem];
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
||||
file = blobToFile(blob, `${file.name}.txt`);
|
||||
|
||||
fileItem.name = file.name;
|
||||
fileItem.size = file.size;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// During the file upload, file content is automatically extracted.
|
||||
@@ -200,7 +186,7 @@
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
};
|
||||
@@ -398,7 +384,7 @@
|
||||
dir={$settings?.chatDirection ?? 'LTR'}
|
||||
>
|
||||
{#if files.length > 0}
|
||||
<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
|
||||
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
||||
{#each files as file, fileIdx}
|
||||
{#if file.type === 'image'}
|
||||
<div class=" relative group">
|
||||
@@ -411,7 +397,7 @@
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
@@ -453,35 +439,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" flex">
|
||||
<div class="ml-1 self-end mb-1.5 flex space-x-1">
|
||||
<InputMenu
|
||||
{screenCaptureHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5">
|
||||
<div
|
||||
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||
>
|
||||
<RichTextInput
|
||||
bind:value={content}
|
||||
@@ -528,8 +488,36 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="self-end mb-1.5 flex space-x-1 mr-1">
|
||||
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
||||
<div class="ml-1 self-end flex space-x-1">
|
||||
<InputMenu
|
||||
{screenCaptureHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
</div>
|
||||
|
||||
<div class="self-end flex space-x-1 mr-1">
|
||||
{#if content === ''}
|
||||
<Tooltip content={$i18n.t('Record voice')}>
|
||||
<button
|
||||
@@ -591,7 +579,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||
sideOffset={15}
|
||||
alignOffset={-8}
|
||||
side="top"
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
@@ -70,7 +72,7 @@
|
||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
|
||||
>
|
||||
<div
|
||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
|
||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
||||
>
|
||||
<ReactionPicker
|
||||
onClose={() => (showButtons = false)}
|
||||
@@ -136,7 +138,7 @@
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<div
|
||||
class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
||||
class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
||||
>
|
||||
{#if showUserProfile}
|
||||
<ProfilePreview user={message.user}>
|
||||
@@ -151,11 +153,9 @@
|
||||
|
||||
{#if message.created_at}
|
||||
<div
|
||||
class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
|
||||
class="mt-1.5 flex shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
|
||||
>
|
||||
<Tooltip
|
||||
content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
|
||||
>
|
||||
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
|
||||
{dayjs(message.created_at / 1000000).format('HH:mm')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -174,9 +174,7 @@
|
||||
<div
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip
|
||||
content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
|
||||
>
|
||||
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -208,7 +206,7 @@
|
||||
{#if edit}
|
||||
<div class="py-2">
|
||||
<Textarea
|
||||
className=" bg-transparent outline-none w-full resize-none"
|
||||
className=" bg-transparent outline-hidden w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
onKeydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="max-w-full w-[240px] rounded-lg z-[9999] bg-white dark:bg-black dark:text-white shadow-lg"
|
||||
class="max-w-full w-[240px] rounded-lg z-9999 bg-white dark:bg-black dark:text-white shadow-lg"
|
||||
sideOffset={8}
|
||||
{side}
|
||||
{align}
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<slot />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
|
||||
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-9999 shadow-lg dark:text-white"
|
||||
sideOffset={8}
|
||||
{side}
|
||||
{align}
|
||||
@@ -116,7 +116,7 @@
|
||||
<div class="mb-1 px-3 pt-2 pb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
placeholder="Search all emojis"
|
||||
bind:value={search}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||
<div
|
||||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
||||
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||
></div>
|
||||
|
||||
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
|
||||
|
||||
@@ -41,11 +41,13 @@
|
||||
convertMessagesToHistory,
|
||||
copyToClipboard,
|
||||
getMessageContentParts,
|
||||
createMessagesList,
|
||||
extractSentencesForAudio,
|
||||
promptTemplate,
|
||||
splitStream,
|
||||
sleep,
|
||||
removeDetailsWithReasoning
|
||||
removeDetails,
|
||||
getPromptVariables
|
||||
} from '$lib/utils';
|
||||
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
@@ -82,10 +84,12 @@
|
||||
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
import NotificationToast from '../NotificationToast.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
export let chatIdProp = '';
|
||||
|
||||
let loaded = false;
|
||||
let loading = false;
|
||||
|
||||
const eventTarget = new EventTarget();
|
||||
let controlPane;
|
||||
let controlPaneComponent;
|
||||
@@ -114,7 +118,7 @@
|
||||
let selectedToolIds = [];
|
||||
let imageGenerationEnabled = false;
|
||||
let webSearchEnabled = false;
|
||||
|
||||
let codeInterpreterEnabled = false;
|
||||
let chat = null;
|
||||
let tags = [];
|
||||
|
||||
@@ -133,6 +137,7 @@
|
||||
|
||||
$: if (chatIdProp) {
|
||||
(async () => {
|
||||
loading = true;
|
||||
console.log(chatIdProp);
|
||||
|
||||
prompt = '';
|
||||
@@ -141,11 +146,9 @@
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
|
||||
loaded = false;
|
||||
|
||||
if (chatIdProp && (await loadChat())) {
|
||||
await tick();
|
||||
loaded = true;
|
||||
loading = false;
|
||||
|
||||
if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
|
||||
try {
|
||||
@@ -184,15 +187,20 @@
|
||||
setToolIds();
|
||||
}
|
||||
|
||||
$: if (atSelectedModel || selectedModels) {
|
||||
setToolIds();
|
||||
}
|
||||
|
||||
const setToolIds = async () => {
|
||||
if (!$tools) {
|
||||
tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
||||
if (selectedModels.length !== 1) {
|
||||
if (selectedModels.length !== 1 && !atSelectedModel) {
|
||||
return;
|
||||
}
|
||||
const model = $models.find((m) => m.id === selectedModels[0]);
|
||||
|
||||
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
|
||||
if (model) {
|
||||
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
|
||||
$tools.find((t) => t.id === id)
|
||||
@@ -223,7 +231,7 @@
|
||||
}
|
||||
|
||||
await tick();
|
||||
saveChatHandler(_chatId);
|
||||
saveChatHandler(_chatId, history);
|
||||
};
|
||||
|
||||
const chatEventHandler = async (event, cb) => {
|
||||
@@ -627,7 +635,7 @@
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -715,6 +723,7 @@
|
||||
if ($page.url.searchParams.get('web-search') === 'true') {
|
||||
webSearchEnabled = true;
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('image-generation') === 'true') {
|
||||
imageGenerationEnabled = true;
|
||||
}
|
||||
@@ -823,20 +832,6 @@
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const createMessagesList = (responseMessageId) => {
|
||||
if (responseMessageId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const message = history.messages[responseMessageId];
|
||||
if (message?.parentId) {
|
||||
return [...createMessagesList(message.parentId), message];
|
||||
} else {
|
||||
return [message];
|
||||
}
|
||||
};
|
||||
|
||||
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
|
||||
const res = await chatCompleted(localStorage.token, {
|
||||
model: modelId,
|
||||
@@ -846,8 +841,10 @@
|
||||
content: m.content,
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp,
|
||||
...(m.usage ? { usage: m.usage } : {}),
|
||||
...(m.sources ? { sources: m.sources } : {})
|
||||
})),
|
||||
model_item: $models.find((m) => m.id === modelId),
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
id: responseMessageId
|
||||
@@ -893,7 +890,7 @@
|
||||
};
|
||||
|
||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
const messages = createMessagesList(history, responseMessageId);
|
||||
|
||||
const res = await chatAction(localStorage.token, actionId, {
|
||||
model: modelId,
|
||||
@@ -906,6 +903,7 @@
|
||||
...(m.sources ? { sources: m.sources } : {})
|
||||
})),
|
||||
...(event ? { event: event } : {}),
|
||||
model_item: $models.find((m) => m.id === modelId),
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
id: responseMessageId
|
||||
@@ -962,7 +960,7 @@
|
||||
const modelId = selectedModels[0];
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
const messages = createMessagesList(history.currentId);
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const parentMessage = messages.length !== 0 ? messages.at(-1) : null;
|
||||
|
||||
const userMessageId = uuidv4();
|
||||
@@ -1007,9 +1005,9 @@
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
await initChatHandler();
|
||||
await initChatHandler(history);
|
||||
} else {
|
||||
await saveChatHandler($chatId);
|
||||
await saveChatHandler($chatId, history);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1071,9 +1069,9 @@
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
await initChatHandler();
|
||||
await initChatHandler(history);
|
||||
} else {
|
||||
await saveChatHandler($chatId);
|
||||
await saveChatHandler($chatId, history);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1207,7 +1205,12 @@
|
||||
);
|
||||
|
||||
history.messages[message.id] = message;
|
||||
await chatCompletedHandler(chatId, message.model, message.id, createMessagesList(message.id));
|
||||
await chatCompletedHandler(
|
||||
chatId,
|
||||
message.model,
|
||||
message.id,
|
||||
createMessagesList(history, message.id)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
@@ -1223,7 +1226,7 @@
|
||||
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
||||
console.log('submitPrompt', userPrompt, $chatId);
|
||||
|
||||
const messages = createMessagesList(history.currentId);
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const _selectedModels = selectedModels.map((modelId) =>
|
||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||
);
|
||||
@@ -1231,7 +1234,7 @@
|
||||
selectedModels = _selectedModels;
|
||||
}
|
||||
|
||||
if (userPrompt === '') {
|
||||
if (userPrompt === '' && files.length === 0) {
|
||||
toast.error($i18n.t('Please enter a prompt'));
|
||||
return;
|
||||
}
|
||||
@@ -1244,7 +1247,7 @@
|
||||
// Response not done
|
||||
return;
|
||||
}
|
||||
if (messages.length != 0 && messages.at(-1).error) {
|
||||
if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
|
||||
// Error in response
|
||||
toast.error($i18n.t(`Oops! There was an error in the previous response.`));
|
||||
return;
|
||||
@@ -1271,13 +1274,14 @@
|
||||
}
|
||||
|
||||
prompt = '';
|
||||
await tick();
|
||||
|
||||
// Reset chat input textarea
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
if (chatInputElement) {
|
||||
await tick();
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 320) + 'px';
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
@@ -1313,34 +1317,25 @@
|
||||
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
|
||||
}
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// focus on chat input
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
saveSessionSelectedModels();
|
||||
|
||||
await sendPrompt(userPrompt, userMessageId, { newChat: true });
|
||||
await sendPrompt(history, userPrompt, userMessageId, { newChat: true });
|
||||
};
|
||||
|
||||
const sendPrompt = async (
|
||||
_history,
|
||||
prompt: string,
|
||||
parentId: string,
|
||||
{ modelId = null, modelIdx = null, newChat = false } = {}
|
||||
) => {
|
||||
// Create new chat if newChat is true and first user message
|
||||
if (
|
||||
newChat &&
|
||||
history.messages[history.currentId].parentId === null &&
|
||||
history.messages[history.currentId].role === 'user'
|
||||
) {
|
||||
await initChatHandler();
|
||||
} else {
|
||||
await saveChatHandler($chatId);
|
||||
}
|
||||
let _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
_history = JSON.parse(JSON.stringify(_history));
|
||||
|
||||
const responseMessageIds: Record<PropertyKey, string> = {};
|
||||
// If modelId is provided, use it, else use selected model
|
||||
let selectedModelIds = modelId
|
||||
? [modelId]
|
||||
@@ -1349,7 +1344,6 @@
|
||||
: selectedModels;
|
||||
|
||||
// Create response messages for each selected model
|
||||
const responseMessageIds: Record<PropertyKey, string> = {};
|
||||
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
@@ -1384,19 +1378,26 @@
|
||||
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
|
||||
}
|
||||
}
|
||||
history = history;
|
||||
|
||||
// Create new chat if newChat is true and first user message
|
||||
if (newChat && _history.messages[_history.currentId].parentId === null) {
|
||||
_chatId = await initChatHandler(_history);
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
||||
_history = JSON.parse(JSON.stringify(history));
|
||||
// Save chat after all messages have been created
|
||||
await saveChatHandler($chatId);
|
||||
await saveChatHandler(_chatId, _history);
|
||||
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
await Promise.all(
|
||||
selectedModelIds.map(async (modelId, _modelIdx) => {
|
||||
console.log('modelId', modelId);
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
if (model) {
|
||||
const messages = createMessagesList(parentId);
|
||||
const messages = createMessagesList(_history, parentId);
|
||||
// If there are image files, check if model is vision capable
|
||||
const hasImages = messages.some((message) =>
|
||||
message.files?.some((file) => file.type === 'image')
|
||||
@@ -1412,7 +1413,7 @@
|
||||
|
||||
let responseMessageId =
|
||||
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
|
||||
let responseMessage = history.messages[responseMessageId];
|
||||
let responseMessage = _history.messages[responseMessageId];
|
||||
|
||||
let userContext = null;
|
||||
if ($settings?.memory ?? false) {
|
||||
@@ -1441,7 +1442,7 @@
|
||||
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
|
||||
|
||||
scrollToBottom();
|
||||
await sendPromptSocket(model, responseMessageId, _chatId);
|
||||
await sendPromptSocket(_history, model, responseMessageId, _chatId);
|
||||
|
||||
if (chatEventEmitter) clearInterval(chatEventEmitter);
|
||||
} else {
|
||||
@@ -1454,9 +1455,9 @@
|
||||
chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
};
|
||||
|
||||
const sendPromptSocket = async (model, responseMessageId, _chatId) => {
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
const userMessage = history.messages[responseMessage.parentId];
|
||||
const sendPromptSocket = async (_history, model, responseMessageId, _chatId) => {
|
||||
const responseMessage = _history.messages[responseMessageId];
|
||||
const userMessage = _history.messages[responseMessage.parentId];
|
||||
|
||||
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||
files.push(
|
||||
@@ -1487,7 +1488,7 @@
|
||||
params?.stream_response ??
|
||||
true;
|
||||
|
||||
const messages = [
|
||||
let messages = [
|
||||
params?.system || $settings.system || (responseMessage?.userContext ?? null)
|
||||
? {
|
||||
role: 'system',
|
||||
@@ -1495,7 +1496,10 @@
|
||||
params?.system ?? $settings?.system ?? '',
|
||||
$user.name,
|
||||
$settings?.userLocation
|
||||
? await getAndUpdateUserLocation(localStorage.token)
|
||||
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
|
||||
console.error(err);
|
||||
return undefined;
|
||||
})
|
||||
: undefined
|
||||
)}${
|
||||
(responseMessage?.userContext ?? null)
|
||||
@@ -1504,12 +1508,13 @@
|
||||
}`
|
||||
}
|
||||
: undefined,
|
||||
...createMessagesList(responseMessageId).map((message) => ({
|
||||
...createMessagesList(_history, responseMessageId).map((message) => ({
|
||||
...message,
|
||||
content: removeDetailsWithReasoning(message.content)
|
||||
content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
|
||||
}))
|
||||
]
|
||||
.filter((message) => message?.content?.trim())
|
||||
].filter((message) => message);
|
||||
|
||||
messages = messages
|
||||
.map((message, idx, arr) => ({
|
||||
role: message.role,
|
||||
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
|
||||
@@ -1533,7 +1538,8 @@
|
||||
: {
|
||||
content: message?.merged?.content ?? message.content
|
||||
})
|
||||
}));
|
||||
}))
|
||||
.filter((message) => message?.role === 'user' || message?.content?.trim());
|
||||
|
||||
const res = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
@@ -1557,10 +1563,36 @@
|
||||
|
||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
|
||||
features: {
|
||||
image_generation: imageGenerationEnabled,
|
||||
web_search: webSearchEnabled
|
||||
image_generation:
|
||||
$config?.features?.enable_image_generation &&
|
||||
($user.role === 'admin' || $user?.permissions?.features?.image_generation)
|
||||
? imageGenerationEnabled
|
||||
: false,
|
||||
code_interpreter:
|
||||
$config?.features?.enable_code_interpreter &&
|
||||
($user.role === 'admin' || $user?.permissions?.features?.code_interpreter)
|
||||
? codeInterpreterEnabled
|
||||
: false,
|
||||
web_search:
|
||||
$config?.features?.enable_web_search &&
|
||||
($user.role === 'admin' || $user?.permissions?.features?.web_search)
|
||||
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
|
||||
: false
|
||||
},
|
||||
variables: {
|
||||
...getPromptVariables(
|
||||
$user.name,
|
||||
$settings?.userLocation
|
||||
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
|
||||
console.error(err);
|
||||
return undefined;
|
||||
})
|
||||
: undefined
|
||||
)
|
||||
},
|
||||
model_item: $models.find((m) => m.id === model.id),
|
||||
|
||||
session_id: $socket?.id,
|
||||
chat_id: $chatId,
|
||||
@@ -1571,7 +1603,7 @@
|
||||
(messages.length == 2 &&
|
||||
messages.at(0)?.role === 'system' &&
|
||||
messages.at(1)?.role === 'user')) &&
|
||||
selectedModels[0] === model.id
|
||||
(selectedModels[0] === model.id || atSelectedModel !== undefined)
|
||||
? {
|
||||
background_tasks: {
|
||||
title_generation: $settings?.title?.auto ?? true,
|
||||
@@ -1590,12 +1622,15 @@
|
||||
},
|
||||
`${WEBUI_BASE_URL}/api`
|
||||
).catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(`${error}`);
|
||||
|
||||
responseMessage.error = {
|
||||
content: error
|
||||
};
|
||||
responseMessage.done = true;
|
||||
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
history.currentId = responseMessageId;
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -1693,7 +1728,7 @@
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
await sendPrompt(history, userPrompt, userMessageId);
|
||||
};
|
||||
|
||||
const regenerateResponse = async (message) => {
|
||||
@@ -1705,11 +1740,11 @@
|
||||
|
||||
if ((userMessage?.models ?? [...selectedModels]).length == 1) {
|
||||
// If user message has only one model selected, sendPrompt automatically selects it for regeneration
|
||||
await sendPrompt(userPrompt, userMessage.id);
|
||||
await sendPrompt(history, userPrompt, userMessage.id);
|
||||
} else {
|
||||
// If there are multiple models selected, use the model of the response message for regeneration
|
||||
// e.g. many model chat
|
||||
await sendPrompt(userPrompt, userMessage.id, {
|
||||
await sendPrompt(history, userPrompt, userMessage.id, {
|
||||
modelId: message.model,
|
||||
modelIdx: message.modelIdx
|
||||
});
|
||||
@@ -1731,7 +1766,7 @@
|
||||
.at(0);
|
||||
|
||||
if (model) {
|
||||
await sendPromptSocket(model, responseMessage.id, _chatId);
|
||||
await sendPromptSocket(history, model, responseMessage.id, _chatId);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1774,7 +1809,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
await saveChatHandler(_chatId);
|
||||
await saveChatHandler(_chatId, history);
|
||||
} else {
|
||||
console.error(res);
|
||||
}
|
||||
@@ -1783,42 +1818,48 @@
|
||||
}
|
||||
};
|
||||
|
||||
const initChatHandler = async () => {
|
||||
const initChatHandler = async (history) => {
|
||||
let _chatId = $chatId;
|
||||
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
id: _chatId,
|
||||
title: $i18n.t('New Chat'),
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
params: params,
|
||||
history: history,
|
||||
messages: createMessagesList(history.currentId),
|
||||
messages: createMessagesList(history, history.currentId),
|
||||
tags: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await chatId.set(chat.id);
|
||||
_chatId = chat.id;
|
||||
await chatId.set(_chatId);
|
||||
|
||||
window.history.replaceState(history.state, '', `/c/${chat.id}`);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
currentChatPage.set(1);
|
||||
|
||||
window.history.replaceState(history.state, '', `/c/${_chatId}`);
|
||||
} else {
|
||||
_chatId = 'local';
|
||||
await chatId.set('local');
|
||||
}
|
||||
await tick();
|
||||
|
||||
return _chatId;
|
||||
};
|
||||
|
||||
const saveChatHandler = async (_chatId) => {
|
||||
const saveChatHandler = async (_chatId, history) => {
|
||||
if ($chatId == _chatId) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
models: selectedModels,
|
||||
history: history,
|
||||
messages: createMessagesList(history.currentId),
|
||||
messages: createMessagesList(history, history.currentId),
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
@@ -1861,7 +1902,7 @@
|
||||
: ' '} w-full max-w-full flex flex-col"
|
||||
id="chat-container"
|
||||
>
|
||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||
{#if chatIdProp === '' || (!loading && chatIdProp)}
|
||||
{#if $settings?.backgroundImageUrl ?? null}
|
||||
<div
|
||||
class="absolute {$showSidebar
|
||||
@@ -1871,7 +1912,7 @@
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
|
||||
class="absolute top-0 left-0 w-full h-full bg-linear-to-t from-white to-white/85 dark:from-gray-900 dark:to-gray-900/90 z-0"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1899,6 +1940,30 @@
|
||||
{#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
|
||||
<div class="absolute top-12 left-0 right-0 w-full z-30">
|
||||
<div class=" flex flex-col gap-1 w-full">
|
||||
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'info',
|
||||
title: 'Trial License',
|
||||
content: $i18n.t(
|
||||
'You are currently using a trial license. Please contact support to upgrade your license.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'error',
|
||||
title: 'License Error',
|
||||
content: $i18n.t(
|
||||
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||
<Banner
|
||||
{banner}
|
||||
@@ -1921,8 +1986,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col flex-auto z-10 w-full">
|
||||
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history.currentId).length > 0}
|
||||
<div class="flex flex-col flex-auto z-10 w-full @container">
|
||||
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history, history.currentId).length > 0}
|
||||
<div
|
||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
|
||||
id="messages-container"
|
||||
@@ -1940,6 +2005,7 @@
|
||||
bind:autoScroll
|
||||
bind:prompt
|
||||
{selectedModels}
|
||||
{atSelectedModel}
|
||||
{sendPrompt}
|
||||
{showMessage}
|
||||
{submitMessage}
|
||||
@@ -1962,6 +2028,7 @@
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
@@ -1986,7 +2053,7 @@
|
||||
}
|
||||
}}
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
if (e.detail || files.length > 0) {
|
||||
await tick();
|
||||
submitPrompt(
|
||||
($settings?.richTextInput ?? true)
|
||||
@@ -2013,6 +2080,7 @@
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
@@ -2028,7 +2096,7 @@
|
||||
}
|
||||
}}
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
if (e.detail || files.length > 0) {
|
||||
await tick();
|
||||
submitPrompt(
|
||||
($settings?.richTextInput ?? true)
|
||||
@@ -2065,5 +2133,11 @@
|
||||
{eventTarget}
|
||||
/>
|
||||
</PaneGroup>
|
||||
{:else if loading}
|
||||
<div class=" flex items-center justify-center h-full w-full">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
|
||||
{#if $showControls}
|
||||
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||
</div>
|
||||
</PaneResizer>
|
||||
@@ -230,7 +230,7 @@
|
||||
<div
|
||||
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
||||
? ' '
|
||||
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<div class="w-full h-full flex justify-center">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
export let modelIds = [];
|
||||
export let models = [];
|
||||
export let atSelectedModel;
|
||||
|
||||
export let submitPrompt;
|
||||
|
||||
@@ -126,7 +127,8 @@
|
||||
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
||||
<Suggestions
|
||||
className="grid grid-cols-2"
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
|
||||
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
on:select={(e) => {
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
toast.error('Model not selected');
|
||||
return;
|
||||
}
|
||||
prompt = `Explain this section to me in more detail\n\n\`\`\`\n${selectedText}\n\`\`\``;
|
||||
const explainText = $i18n.t('Explain this section to me in more detail');
|
||||
prompt = `${explainText}\n\n\`\`\`\n${selectedText}\n\`\`\``;
|
||||
|
||||
responseContent = '';
|
||||
const [res, controller] = await chatCompletion(localStorage.token, {
|
||||
@@ -221,7 +222,7 @@
|
||||
|
||||
<div
|
||||
id={`floating-buttons-${id}`}
|
||||
class="absolute rounded-lg mt-1 text-xs z-[9999]"
|
||||
class="absolute rounded-lg mt-1 text-xs z-9999"
|
||||
style="display: none"
|
||||
>
|
||||
{#if responseContent === null}
|
||||
@@ -230,7 +231,7 @@
|
||||
class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
|
||||
>
|
||||
<button
|
||||
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
|
||||
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
|
||||
on:click={async () => {
|
||||
selectedText = window.getSelection().toString();
|
||||
floatingInput = true;
|
||||
@@ -246,10 +247,10 @@
|
||||
>
|
||||
<ChatBubble className="size-3 shrink-0" />
|
||||
|
||||
<div class="shrink-0">Ask</div>
|
||||
<div class="shrink-0">{$i18n.t('Ask')}</div>
|
||||
</button>
|
||||
<button
|
||||
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
|
||||
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
|
||||
on:click={() => {
|
||||
selectedText = window.getSelection().toString();
|
||||
explainHandler();
|
||||
@@ -257,17 +258,17 @@
|
||||
>
|
||||
<LightBlub className="size-3 shrink-0" />
|
||||
|
||||
<div class="shrink-0">Explain</div>
|
||||
<div class="shrink-0">{$i18n.t('Explain')}</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-800 w-72 rounded-full shadow-xl"
|
||||
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-850 w-72 rounded-full shadow-xl"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="floating-message-input"
|
||||
class="ml-5 bg-transparent outline-none w-full flex-1 text-sm"
|
||||
class="ml-5 bg-transparent outline-hidden w-full flex-1 text-sm"
|
||||
placeholder={$i18n.t('Ask a question')}
|
||||
bind:value={floatingInputValue}
|
||||
on:keydown={(e) => {
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<div class="" slot="content">
|
||||
<textarea
|
||||
bind:value={params.system}
|
||||
class="w-full text-xs py-1.5 bg-transparent outline-none resize-none"
|
||||
class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt')}
|
||||
/>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
|
||||
class=" w-full rounded-sm text-xs py-2 px-1 bg-transparent outline-hidden"
|
||||
bind:value={tab}
|
||||
placeholder="Select"
|
||||
>
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
|
||||
class="w-full rounded-sm py-2 px-1 text-xs bg-transparent outline-hidden"
|
||||
bind:value={selectedId}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
|
||||
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
|
||||
|
||||
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -16,29 +17,35 @@
|
||||
showCallOverlay,
|
||||
tools,
|
||||
user as _user,
|
||||
showControls
|
||||
showControls,
|
||||
TTSWorker
|
||||
} from '$lib/stores';
|
||||
|
||||
import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import { uploadFile } from '$lib/apis/files';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
import { generateAutoCompletion } from '$lib/apis';
|
||||
import { deleteFileById } from '$lib/apis/files';
|
||||
|
||||
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||
import Commands from './MessageInput/Commands.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
import { generateAutoCompletion } from '$lib/apis';
|
||||
import { error, text } from '@sveltejs/kit';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import Image from '../common/Image.svelte';
|
||||
import { deleteFileById } from '$lib/apis/files';
|
||||
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
import GlobeAlt from '../icons/GlobeAlt.svelte';
|
||||
import PhotoSolid from '../icons/PhotoSolid.svelte';
|
||||
import Photo from '../icons/Photo.svelte';
|
||||
import CommandLine from '../icons/CommandLine.svelte';
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -65,6 +72,7 @@
|
||||
|
||||
export let imageGenerationEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
|
||||
$: onChange({
|
||||
prompt,
|
||||
@@ -166,22 +174,6 @@
|
||||
}
|
||||
|
||||
files = [...files, fileItem];
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
||||
file = blobToFile(blob, `${file.name}.txt`);
|
||||
|
||||
fileItem.name = file.name;
|
||||
fileItem.size = file.size;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// During the file upload, file content is automatically extracted.
|
||||
@@ -211,7 +203,7 @@
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
};
|
||||
@@ -242,7 +234,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
if (visionCapableModels.length === 0) {
|
||||
toast.error($i18n.t('Selected model(s) do not support image inputs'));
|
||||
return;
|
||||
@@ -385,9 +379,9 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full relative">
|
||||
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || imageGenerationEnabled}
|
||||
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled}
|
||||
<div
|
||||
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
|
||||
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-linear-to-t from-white dark:from-gray-900 z-10"
|
||||
>
|
||||
{#if selectedToolIds.length > 0}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
@@ -406,7 +400,7 @@
|
||||
}) as tool, toolIdx (toolIdx)}
|
||||
<Tooltip
|
||||
content={tool?.meta?.description ?? ''}
|
||||
className=" {toolIdx !== 0 ? 'pl-0.5' : ''} flex-shrink-0"
|
||||
className=" {toolIdx !== 0 ? 'pl-0.5' : ''} shrink-0"
|
||||
placement="top"
|
||||
>
|
||||
{tool.name}
|
||||
@@ -421,23 +415,39 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if webSearchEnabled || ($config?.features?.enable_web_search && ($settings?.webSearch ?? false)) === 'always'}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
||||
<div class="pl-1">
|
||||
<span class="relative flex size-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"
|
||||
/>
|
||||
<span class="relative inline-flex rounded-full size-2 bg-blue-500" />
|
||||
</span>
|
||||
</div>
|
||||
<div class=" translate-y-[0.5px]">{$i18n.t('Search the internet')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if imageGenerationEnabled}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
||||
<div class="pl-1">
|
||||
<span class="relative flex size-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-teal-400 opacity-75"
|
||||
/>
|
||||
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||
<span class="relative inline-flex rounded-full size-2 bg-teal-500" />
|
||||
</span>
|
||||
</div>
|
||||
<div class=" ">{$i18n.t('Image generation')}</div>
|
||||
<div class=" translate-y-[0.5px]">{$i18n.t('Generate an image')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if webSearchEnabled}
|
||||
{#if codeInterpreterEnabled}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
|
||||
<div class="pl-1">
|
||||
@@ -448,7 +458,7 @@
|
||||
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||
</span>
|
||||
</div>
|
||||
<div class=" ">{$i18n.t('Search the web')}</div>
|
||||
<div class=" translate-y-[0.5px]">{$i18n.t('Execute code for analysis')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -568,7 +578,7 @@
|
||||
dir={$settings?.chatDirection ?? 'LTR'}
|
||||
>
|
||||
{#if files.length > 0}
|
||||
<div class="mx-1 mt-2.5 mb-1 flex items-center flex-wrap gap-2">
|
||||
<div class="mx-2 mt-2.5 -mb-1 flex items-center flex-wrap gap-2">
|
||||
{#each files as file, fileIdx}
|
||||
{#if file.type === 'image'}
|
||||
<div class=" relative group">
|
||||
@@ -606,7 +616,7 @@
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
@@ -617,7 +627,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
class="size-4"
|
||||
>
|
||||
<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"
|
||||
@@ -656,80 +666,26 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" flex">
|
||||
<div class="ml-1 self-end mb-1.5 flex space-x-1">
|
||||
<InputMenu
|
||||
bind:imageGenerationEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:selectedToolIds
|
||||
{screenCaptureHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
uploadGoogleDriveHandler={async () => {
|
||||
try {
|
||||
const fileData = await createPicker();
|
||||
if (fileData) {
|
||||
const file = new File([fileData.blob], fileData.name, {
|
||||
type: fileData.blob.type
|
||||
});
|
||||
await uploadFileHandler(file);
|
||||
} else {
|
||||
console.log('No file was selected from Google Drive');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Google Drive Error:', error);
|
||||
toast.error(
|
||||
$i18n.t('Error accessing Google Drive: {{error}}', {
|
||||
error: error.message
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
onClose={async () => {
|
||||
await tick();
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5">
|
||||
{#if $settings?.richTextInput ?? true}
|
||||
<div
|
||||
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
|
||||
>
|
||||
<RichTextInput
|
||||
bind:this={chatInputElement}
|
||||
bind:value={prompt}
|
||||
id="chat-input"
|
||||
messageInput={true}
|
||||
shiftEnter={!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)}
|
||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||
(!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
))}
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||
autocomplete={true}
|
||||
autocomplete={$config?.features.enable_autocomplete_generation}
|
||||
generateAutoCompletion={async (text) => {
|
||||
if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
|
||||
toast.error($i18n.t('Please select a model first.'));
|
||||
@@ -850,15 +806,20 @@
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards.
|
||||
//
|
||||
// Depending on the user's settings, it will send the message
|
||||
// either when Enter is pressed or when Ctrl+Enter is pressed.
|
||||
const enterPressed =
|
||||
($settings?.ctrlEnterToSend ?? false)
|
||||
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
|
||||
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) {
|
||||
dispatch('submit', prompt);
|
||||
if (enterPressed) {
|
||||
e.preventDefault();
|
||||
if (prompt !== '' || files.length > 0) {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -918,37 +879,20 @@
|
||||
<textarea
|
||||
id="chat-input"
|
||||
bind:this={chatInputElement}
|
||||
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
|
||||
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
|
||||
console.log('keydown', e);
|
||||
const commandsContainerElement =
|
||||
document.getElementById('commands-container');
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
stopResponse();
|
||||
}
|
||||
|
||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -984,51 +928,83 @@
|
||||
editButton?.click();
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectUp();
|
||||
if (commandsContainerElement) {
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectDown();
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (e.shiftKey) {
|
||||
prompt = `${prompt}\n`;
|
||||
} else if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
}
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (e.shiftKey) {
|
||||
prompt = `${prompt}\n`;
|
||||
} else if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
console.log('keypress', e);
|
||||
// Prevent Enter key from creating a new line
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
||||
const enterPressed =
|
||||
($settings?.ctrlEnterToSend ?? false)
|
||||
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
|
||||
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
|
||||
|
||||
console.log('Enter pressed:', enterPressed);
|
||||
|
||||
if (enterPressed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if ((prompt !== '' || files.length > 0) && enterPressed) {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
commandOptionButton?.click();
|
||||
} else if (e.key === 'Tab') {
|
||||
if (e.key === 'Tab') {
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
@@ -1107,8 +1083,140 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="self-end mb-1.5 flex space-x-1 mr-1">
|
||||
<div class=" flex justify-between mt-1.5 mb-2.5 mx-0.5 max-w-full">
|
||||
<div class="ml-1 self-end gap-0.5 flex items-center flex-1 max-w-[80%]">
|
||||
<InputMenu
|
||||
bind:selectedToolIds
|
||||
{screenCaptureHandler}
|
||||
{inputFilesHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
uploadGoogleDriveHandler={async () => {
|
||||
try {
|
||||
const fileData = await createPicker();
|
||||
if (fileData) {
|
||||
const file = new File([fileData.blob], fileData.name, {
|
||||
type: fileData.blob.type
|
||||
});
|
||||
await uploadFileHandler(file);
|
||||
} else {
|
||||
console.log('No file was selected from Google Drive');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Google Drive Error:', error);
|
||||
toast.error(
|
||||
$i18n.t('Error accessing Google Drive: {{error}}', {
|
||||
error: error.message
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
uploadOneDriveHandler={async () => {
|
||||
try {
|
||||
const fileData = await pickAndDownloadFile();
|
||||
if (fileData) {
|
||||
const file = new File([fileData.blob], fileData.name, {
|
||||
type: fileData.blob.type || 'application/octet-stream'
|
||||
});
|
||||
await uploadFileHandler(file);
|
||||
} else {
|
||||
console.log('No file was selected from OneDrive');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OneDrive Error:', error);
|
||||
}
|
||||
}}
|
||||
onClose={async () => {
|
||||
await tick();
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
|
||||
<div class="flex gap-0.5 items-center overflow-x-auto scrollbar-none flex-1">
|
||||
{#if $_user}
|
||||
{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
|
||||
<Tooltip content={$i18n.t('Search the internet')} placement="top">
|
||||
<button
|
||||
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
|
||||
type="button"
|
||||
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
|
||||
($settings?.webSearch ?? false) === 'always'
|
||||
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
|
||||
>
|
||||
<GlobeAlt className="size-5" strokeWidth="1.75" />
|
||||
<span
|
||||
class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
|
||||
>{$i18n.t('Web Search')}</span
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
|
||||
<Tooltip content={$i18n.t('Generate an image')} placement="top">
|
||||
<button
|
||||
on:click|preventDefault={() =>
|
||||
(imageGenerationEnabled = !imageGenerationEnabled)}
|
||||
type="button"
|
||||
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
|
||||
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
|
||||
>
|
||||
<Photo className="size-5" strokeWidth="1.75" />
|
||||
<span
|
||||
class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
|
||||
>{$i18n.t('Image')}</span
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
|
||||
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
|
||||
<button
|
||||
on:click|preventDefault={() =>
|
||||
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
||||
type="button"
|
||||
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled
|
||||
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
|
||||
>
|
||||
<CommandLine className="size-5" strokeWidth="1.75" />
|
||||
<span
|
||||
class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
|
||||
>{$i18n.t('Code Interpreter')}</span
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="self-end flex space-x-1 mr-1 shrink-0">
|
||||
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
||||
<Tooltip content={$i18n.t('Record voice')}>
|
||||
<button
|
||||
@@ -1159,11 +1267,14 @@
|
||||
{/if}
|
||||
|
||||
{#if !history.currentId || history.messages[history.currentId]?.done == true}
|
||||
{#if prompt === ''}
|
||||
{#if prompt === '' && files.length === 0}
|
||||
<div class=" flex items-center">
|
||||
<Tooltip content={$i18n.t('Call')}>
|
||||
<button
|
||||
class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-2 self-center"
|
||||
class=" {webSearchEnabled ||
|
||||
($settings?.webSearch ?? false) === 'always'
|
||||
? 'bg-blue-500 text-white hover:bg-blue-400 '
|
||||
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100'} transition rounded-full p-1.5 self-center"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (selectedModels.length > 1) {
|
||||
@@ -1195,6 +1306,19 @@
|
||||
|
||||
stream = null;
|
||||
|
||||
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
||||
// If the user has not initialized the TTS worker, initialize it
|
||||
if (!$TTSWorker) {
|
||||
await TTSWorker.set(
|
||||
new KokoroWorker({
|
||||
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
|
||||
})
|
||||
);
|
||||
|
||||
await $TTSWorker.init();
|
||||
}
|
||||
}
|
||||
|
||||
showCallOverlay.set(true);
|
||||
showControls.set(true);
|
||||
} catch (err) {
|
||||
@@ -1215,17 +1339,19 @@
|
||||
<Tooltip content={$i18n.t('Send message')}>
|
||||
<button
|
||||
id="send-message-button"
|
||||
class="{prompt !== ''
|
||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
class="{!(prompt === '' && files.length === 0)
|
||||
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
|
||||
? 'bg-blue-500 text-white hover:bg-blue-400 '
|
||||
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
||||
type="submit"
|
||||
disabled={prompt === ''}
|
||||
disabled={prompt === '' && files.length === 0}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -1250,7 +1376,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { config, models, settings, showCallOverlay } from '$lib/stores';
|
||||
import { config, models, settings, showCallOverlay, TTSWorker } from '$lib/stores';
|
||||
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -230,7 +231,6 @@
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
audioChunks = [];
|
||||
analyseAudio(audioStream);
|
||||
};
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
@@ -244,7 +244,7 @@
|
||||
stopRecordingCallback();
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
analyseAudio(audioStream);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
if (hasSound) {
|
||||
// BIG RED TEXT
|
||||
console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected');
|
||||
if (mediaRecorder && mediaRecorder.state !== 'recording') {
|
||||
mediaRecorder.start();
|
||||
}
|
||||
|
||||
if (!hasStartedSpeaking) {
|
||||
hasStartedSpeaking = true;
|
||||
@@ -459,7 +462,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ($config.audio.tts.engine !== '') {
|
||||
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
||||
const blob = await $TTSWorker
|
||||
.generate({
|
||||
text: content,
|
||||
voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
|
||||
if (blob) {
|
||||
audioCache.set(content, new Audio(blob));
|
||||
}
|
||||
} else if ($config.audio.tts.engine !== '') {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm"
|
||||
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
|
||||
sideOffset={6}
|
||||
side="top"
|
||||
align="start"
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
@@ -185,25 +185,25 @@
|
||||
<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
|
||||
{#if item.legacy}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
Legacy
|
||||
</div>
|
||||
{:else if item?.meta?.document}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
Document
|
||||
</div>
|
||||
{:else if item?.type === 'file'}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
File
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
|
||||
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
Collection
|
||||
</div>
|
||||
@@ -238,7 +238,7 @@
|
||||
class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
File
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
|
||||
@@ -74,7 +74,13 @@
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_LOCATION}}')) {
|
||||
const location = await getUserPosition();
|
||||
let location;
|
||||
try {
|
||||
location = await getUserPosition();
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Location access not allowed'));
|
||||
location = 'LOCATION_UNKNOWN';
|
||||
}
|
||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||
}
|
||||
|
||||
@@ -139,7 +145,7 @@
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
bind:this={overlayElement}
|
||||
class="fixed {$showSidebar
|
||||
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
||||
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
|
||||
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-9999 touch-none pointer-events-none"
|
||||
id="dropzone"
|
||||
role="region"
|
||||
aria-label="Drag and Drop Container"
|
||||
>
|
||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<AddFilesPlaceholder />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
@@ -15,39 +16,31 @@
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
|
||||
import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
|
||||
import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let screenCaptureHandler: Function;
|
||||
export let uploadFilesHandler: Function;
|
||||
export let inputFilesHandler: Function;
|
||||
|
||||
export let uploadGoogleDriveHandler: Function;
|
||||
export let uploadOneDriveHandler: Function;
|
||||
|
||||
export let selectedToolIds: string[] = [];
|
||||
|
||||
export let webSearchEnabled: boolean;
|
||||
export let imageGenerationEnabled: boolean;
|
||||
|
||||
export let onClose: Function;
|
||||
|
||||
let tools = {};
|
||||
let show = false;
|
||||
|
||||
let showImageGeneration = false;
|
||||
|
||||
$: showImageGeneration =
|
||||
$config?.features?.enable_image_generation &&
|
||||
($user.role === 'admin' || $user?.permissions?.features?.image_generation);
|
||||
|
||||
let showWebSearch = false;
|
||||
|
||||
$: showWebSearch =
|
||||
$config?.features?.enable_web_search &&
|
||||
($user.role === 'admin' || $user?.permissions?.features?.web_search);
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
let fileUploadEnabled = true;
|
||||
$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
|
||||
|
||||
const init = async () => {
|
||||
if ($_tools === null) {
|
||||
await _tools.set(await getTools(localStorage.token));
|
||||
@@ -62,8 +55,31 @@
|
||||
return a;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const detectMobile = () => {
|
||||
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
|
||||
};
|
||||
|
||||
function handleFileChange(event) {
|
||||
const inputFiles = Array.from(event.target?.files);
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
console.log(inputFiles);
|
||||
inputFilesHandler(inputFiles);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Hidden file input used to open the camera on mobile -->
|
||||
<input
|
||||
id="camera-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
on:change={handleFileChange}
|
||||
style="display: none;"
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
@@ -78,7 +94,7 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[220px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[220px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||
sideOffset={15}
|
||||
alignOffset={-8}
|
||||
side="top"
|
||||
@@ -100,7 +116,7 @@
|
||||
placement="top-start"
|
||||
className="flex flex-1 gap-2 items-center"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<WrenchSolid />
|
||||
</div>
|
||||
|
||||
@@ -108,7 +124,7 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class=" flex-shrink-0">
|
||||
<div class=" shrink-0">
|
||||
<Switch
|
||||
state={tools[toolId].enabled}
|
||||
on:change={async (e) => {
|
||||
@@ -129,63 +145,51 @@
|
||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||
{/if}
|
||||
|
||||
{#if showImageGeneration}
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
on:click={() => {
|
||||
imageGenerationEnabled = !imageGenerationEnabled;
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<PhotoSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Image')}</div>
|
||||
</div>
|
||||
|
||||
<Switch state={imageGenerationEnabled} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if showWebSearch}
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
on:click={() => {
|
||||
webSearchEnabled = !webSearchEnabled;
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<GlobeAltSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
|
||||
</div>
|
||||
|
||||
<Switch state={webSearchEnabled} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if showImageGeneration || showWebSearch}
|
||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||
{/if}
|
||||
|
||||
{#if !$mobile}
|
||||
<Tooltip
|
||||
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
|
||||
className="w-full"
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
screenCaptureHandler();
|
||||
if (fileUploadEnabled) {
|
||||
if (!detectMobile()) {
|
||||
screenCaptureHandler();
|
||||
} else {
|
||||
const cameraInputElement = document.getElementById('camera-input');
|
||||
|
||||
if (cameraInputElement) {
|
||||
cameraInputElement.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CameraSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
on:click={() => {
|
||||
uploadFilesHandler();
|
||||
}}
|
||||
<Tooltip
|
||||
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
|
||||
className="w-full"
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
uploadFilesHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip>
|
||||
|
||||
{#if $config?.features?.enable_google_drive_integration}
|
||||
<DropdownMenu.Item
|
||||
@@ -223,6 +227,97 @@
|
||||
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_onedrive_integration}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
on:click={() => {
|
||||
uploadOneDriveHandler();
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
|
||||
<mask
|
||||
id="mask0_87_7796"
|
||||
style="mask-type:alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="6"
|
||||
width="32"
|
||||
height="20"
|
||||
>
|
||||
<path
|
||||
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
|
||||
fill="#C4C4C4"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_87_7796)">
|
||||
<path
|
||||
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
|
||||
fill="url(#paint0_linear_87_7796)"
|
||||
/>
|
||||
<path
|
||||
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
|
||||
fill="url(#paint1_linear_87_7796)"
|
||||
/>
|
||||
<path
|
||||
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
|
||||
fill="url(#paint2_linear_87_7796)"
|
||||
/>
|
||||
<path
|
||||
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
|
||||
fill="url(#paint3_linear_87_7796)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_87_7796"
|
||||
x1="4.42591"
|
||||
y1="24.6668"
|
||||
x2="27.2309"
|
||||
y2="23.2764"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#2086B8" />
|
||||
<stop offset="1" stop-color="#46D3F6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_87_7796"
|
||||
x1="23.8302"
|
||||
y1="19.6668"
|
||||
x2="30.2108"
|
||||
y2="15.2082"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#1694DB" />
|
||||
<stop offset="1" stop-color="#62C3FE" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_87_7796"
|
||||
x1="8.51037"
|
||||
y1="7.33333"
|
||||
x2="23.3335"
|
||||
y2="15.9348"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#0D3D78" />
|
||||
<stop offset="1" stop-color="#063B83" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_87_7796"
|
||||
x1="-0.340429"
|
||||
y1="19.9998"
|
||||
x2="14.5634"
|
||||
y2="14.4649"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#16589B" />
|
||||
<stop offset="1" stop-color="#1464B7" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
{#each visualizerData.slice().reverse() as rms}
|
||||
<div class="flex items-center h-full">
|
||||
<div
|
||||
class="w-[2px] flex-shrink-0
|
||||
class="w-[2px] shrink-0
|
||||
|
||||
{loading
|
||||
? ' bg-gray-500 dark:bg-gray-400 '
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
|
||||
import {
|
||||
chats,
|
||||
config,
|
||||
settings,
|
||||
user as _user,
|
||||
mobile,
|
||||
currentChatPage,
|
||||
temporaryChatEnabled
|
||||
} from '$lib/stores';
|
||||
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -24,6 +32,7 @@
|
||||
export let prompt;
|
||||
export let history = {};
|
||||
export let selectedModels;
|
||||
export let atSelectedModel;
|
||||
|
||||
let messages = [];
|
||||
|
||||
@@ -85,15 +94,17 @@
|
||||
};
|
||||
|
||||
const updateChat = async () => {
|
||||
history = history;
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history,
|
||||
messages: messages
|
||||
});
|
||||
if (!$temporaryChatEnabled) {
|
||||
history = history;
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history,
|
||||
messages: messages
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
};
|
||||
|
||||
const showPreviousMessage = async (message) => {
|
||||
@@ -217,7 +228,8 @@
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels
|
||||
models: selectedModels,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
@@ -233,7 +245,7 @@
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
await sendPrompt(history, userPrompt, userMessageId);
|
||||
} else {
|
||||
// Edit user message
|
||||
history.messages[messageId].content = content;
|
||||
@@ -339,6 +351,7 @@
|
||||
{#if Object.keys(history?.messages ?? {}).length == 0}
|
||||
<ChatPlaceholder
|
||||
modelIds={selectedModels}
|
||||
{atSelectedModel}
|
||||
submitPrompt={async (p) => {
|
||||
let text = p;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let id = '';
|
||||
export let sources = [];
|
||||
|
||||
let citations = [];
|
||||
@@ -42,6 +43,7 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
console.log('sources', sources);
|
||||
citations = sources.reduce((acc, source) => {
|
||||
if (Object.keys(source).length === 0) {
|
||||
return acc;
|
||||
@@ -52,7 +54,7 @@
|
||||
const distance = source.distances?.[index];
|
||||
|
||||
// Within the same citation there could be multiple documents
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
const id = metadata?.source ?? source?.source?.id ?? 'N/A';
|
||||
let _source = source?.source;
|
||||
|
||||
if (metadata?.name) {
|
||||
@@ -97,11 +99,11 @@
|
||||
{#if citations.length > 0}
|
||||
<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
||||
{#if citations.length <= 3}
|
||||
<div class="flex text-xs font-medium">
|
||||
<div class="flex text-xs font-medium flex-wrap">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
id={`source-${citation.source.name}`}
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
||||
id={`source-${id}-${idx}`}
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
@@ -113,7 +115,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
||||
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
||||
>
|
||||
{citation.source.name}
|
||||
</div>
|
||||
@@ -121,17 +123,26 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Collapsible bind:open={isCollapsibleOpen} className="w-full">
|
||||
<Collapsible
|
||||
id="collapsible-sources"
|
||||
bind:open={isCollapsibleOpen}
|
||||
className="w-full max-w-full "
|
||||
buttonClassName="w-fit max-w-full"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
||||
class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
||||
>
|
||||
<div class="flex-grow flex items-center gap-1 overflow-hidden">
|
||||
<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
|
||||
>
|
||||
<span class="whitespace-nowrap hidden sm:inline shrink-0"
|
||||
>{$i18n.t('References from')}</span
|
||||
>
|
||||
<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
|
||||
<div class="flex text-xs font-medium items-center">
|
||||
{#each citations.slice(0, 2) as citation, idx}
|
||||
<button
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
@@ -145,20 +156,20 @@
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 line-clamp-1 truncate">
|
||||
<div class="flex-1 mx-1 truncate">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
||||
<div class="flex items-center gap-1 whitespace-nowrap shrink-0">
|
||||
<span class="hidden sm:inline">{$i18n.t('and')}</span>
|
||||
{citations.length - 2}
|
||||
<span>{$i18n.t('more')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
{#if isCollapsibleOpen}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
@@ -167,10 +178,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content">
|
||||
<div class="flex text-xs font-medium">
|
||||
<div class="flex text-xs font-medium flex-wrap">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
id={`source-${id}-${idx}`}
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
@@ -181,7 +193,7 @@
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 line-clamp-1 truncate">
|
||||
<div class="flex-1 mx-1 truncate">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
>
|
||||
<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
|
||||
<a
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow"
|
||||
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
|
||||
href={document?.metadata?.file_id
|
||||
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
: document.source?.url?.includes('http')
|
||||
@@ -122,7 +122,9 @@
|
||||
<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
|
||||
{#if showPercentage}
|
||||
{@const percentage = calculatePercentage(document.distance)}
|
||||
<span class={`px-1 rounded font-medium ${getRelevanceColor(percentage)}`}>
|
||||
<span
|
||||
class={`px-1 rounded-sm font-medium ${getRelevanceColor(percentage)}`}
|
||||
>
|
||||
{percentage.toFixed(2)}%
|
||||
</span>
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
@@ -166,7 +168,7 @@
|
||||
</div>
|
||||
|
||||
{#if documentIdx !== mergedDocuments.length - 1}
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-3" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import hljs from 'highlight.js';
|
||||
import { loadPyodide } from 'pyodide';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
|
||||
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import 'highlight.js/styles/github-dark.min.css';
|
||||
@@ -13,23 +11,34 @@
|
||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import { executeCode } from '$lib/apis/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
|
||||
import CommandLine from '$lib/components/icons/CommandLine.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = '';
|
||||
|
||||
export let onSave = (e) => {};
|
||||
export let onCode = (e) => {};
|
||||
|
||||
export let save = false;
|
||||
export let run = true;
|
||||
|
||||
export let token;
|
||||
export let lang = '';
|
||||
export let code = '';
|
||||
export let attributes = {};
|
||||
|
||||
export let className = 'my-2';
|
||||
export let editorClassName = '';
|
||||
export let stickyButtonsClassName = 'top-8';
|
||||
|
||||
let pyodideWorker = null;
|
||||
|
||||
let _code = '';
|
||||
$: if (code) {
|
||||
updateCode();
|
||||
@@ -49,15 +58,21 @@
|
||||
let stdout = null;
|
||||
let stderr = null;
|
||||
let result = null;
|
||||
let files = null;
|
||||
|
||||
let collapsed = false;
|
||||
let copied = false;
|
||||
let saved = false;
|
||||
|
||||
const collapseCodeBlock = () => {
|
||||
collapsed = !collapsed;
|
||||
};
|
||||
|
||||
const saveCode = () => {
|
||||
saved = true;
|
||||
|
||||
code = _code;
|
||||
dispatch('save', code);
|
||||
onSave(code);
|
||||
|
||||
setTimeout(() => {
|
||||
saved = false;
|
||||
@@ -109,95 +124,87 @@
|
||||
};
|
||||
|
||||
const executePython = async (code) => {
|
||||
if (!code.includes('input') && !code.includes('matplotlib')) {
|
||||
executePythonAsWorker(code);
|
||||
} else {
|
||||
result = null;
|
||||
stdout = null;
|
||||
stderr = null;
|
||||
|
||||
executing = true;
|
||||
|
||||
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
|
||||
|
||||
let pyodide = await loadPyodide({
|
||||
indexURL: '/pyodide/',
|
||||
stdout: (text) => {
|
||||
console.log('Python output:', text);
|
||||
|
||||
if (stdout) {
|
||||
stdout += `${text}\n`;
|
||||
} else {
|
||||
stdout = `${text}\n`;
|
||||
}
|
||||
},
|
||||
stderr: (text) => {
|
||||
console.log('An error occurred:', text);
|
||||
if (stderr) {
|
||||
stderr += `${text}\n`;
|
||||
} else {
|
||||
stderr = `${text}\n`;
|
||||
}
|
||||
},
|
||||
packages: ['micropip']
|
||||
});
|
||||
|
||||
try {
|
||||
const micropip = pyodide.pyimport('micropip');
|
||||
|
||||
// await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
|
||||
|
||||
let packages = [
|
||||
code.includes('requests') ? 'requests' : null,
|
||||
code.includes('bs4') ? 'beautifulsoup4' : null,
|
||||
code.includes('numpy') ? 'numpy' : null,
|
||||
code.includes('pandas') ? 'pandas' : null,
|
||||
code.includes('matplotlib') ? 'matplotlib' : null,
|
||||
code.includes('sklearn') ? 'scikit-learn' : null,
|
||||
code.includes('scipy') ? 'scipy' : null,
|
||||
code.includes('re') ? 'regex' : null,
|
||||
code.includes('seaborn') ? 'seaborn' : null
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(packages);
|
||||
await micropip.install(packages);
|
||||
|
||||
result = await pyodide.runPythonAsync(`from js import prompt
|
||||
def input(p):
|
||||
return prompt(p)
|
||||
__builtins__.input = input`);
|
||||
|
||||
result = await pyodide.runPython(code);
|
||||
|
||||
if (!result) {
|
||||
result = '[NO OUTPUT]';
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
|
||||
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
|
||||
|
||||
if (pltCanvasElement?.innerHTML !== '') {
|
||||
pltCanvasElement.classList.add('pt-4');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
stderr = error;
|
||||
}
|
||||
|
||||
executing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executePythonAsWorker = async (code) => {
|
||||
result = null;
|
||||
stdout = null;
|
||||
stderr = null;
|
||||
|
||||
executing = true;
|
||||
|
||||
if ($config?.code?.engine === 'jupyter') {
|
||||
const output = await executeCode(localStorage.token, code).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (output) {
|
||||
if (output['stdout']) {
|
||||
stdout = output['stdout'];
|
||||
const stdoutLines = stdout.split('\n');
|
||||
|
||||
for (const [idx, line] of stdoutLines.entries()) {
|
||||
if (line.startsWith('data:image/png;base64')) {
|
||||
if (files) {
|
||||
files.push({
|
||||
type: 'image/png',
|
||||
data: line
|
||||
});
|
||||
} else {
|
||||
files = [
|
||||
{
|
||||
type: 'image/png',
|
||||
data: line
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (stdout.startsWith(`${line}\n`)) {
|
||||
stdout = stdout.replace(`${line}\n`, ``);
|
||||
} else if (stdout.startsWith(`${line}`)) {
|
||||
stdout = stdout.replace(`${line}`, ``);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (output['result']) {
|
||||
result = output['result'];
|
||||
const resultLines = result.split('\n');
|
||||
|
||||
for (const [idx, line] of resultLines.entries()) {
|
||||
if (line.startsWith('data:image/png;base64')) {
|
||||
if (files) {
|
||||
files.push({
|
||||
type: 'image/png',
|
||||
data: line
|
||||
});
|
||||
} else {
|
||||
files = [
|
||||
{
|
||||
type: 'image/png',
|
||||
data: line
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (result.startsWith(`${line}\n`)) {
|
||||
result = result.replace(`${line}\n`, ``);
|
||||
} else if (result.startsWith(`${line}`)) {
|
||||
result = result.replace(`${line}`, ``);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output['stderr'] && (stderr = output['stderr']);
|
||||
}
|
||||
|
||||
executing = false;
|
||||
} else {
|
||||
executePythonAsWorker(code);
|
||||
}
|
||||
};
|
||||
|
||||
const executePythonAsWorker = async (code) => {
|
||||
let packages = [
|
||||
code.includes('requests') ? 'requests' : null,
|
||||
code.includes('bs4') ? 'beautifulsoup4' : null,
|
||||
@@ -206,12 +213,16 @@ __builtins__.input = input`);
|
||||
code.includes('sklearn') ? 'scikit-learn' : null,
|
||||
code.includes('scipy') ? 'scipy' : null,
|
||||
code.includes('re') ? 'regex' : null,
|
||||
code.includes('seaborn') ? 'seaborn' : null
|
||||
code.includes('seaborn') ? 'seaborn' : null,
|
||||
code.includes('sympy') ? 'sympy' : null,
|
||||
code.includes('tiktoken') ? 'tiktoken' : null,
|
||||
code.includes('matplotlib') ? 'matplotlib' : null,
|
||||
code.includes('pytz') ? 'pytz' : null
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(packages);
|
||||
|
||||
const pyodideWorker = new PyodideWorker();
|
||||
pyodideWorker = new PyodideWorker();
|
||||
|
||||
pyodideWorker.postMessage({
|
||||
id: id,
|
||||
@@ -233,7 +244,64 @@ __builtins__.input = input`);
|
||||
|
||||
console.log(id, data);
|
||||
|
||||
data['stdout'] && (stdout = data['stdout']);
|
||||
if (data['stdout']) {
|
||||
stdout = data['stdout'];
|
||||
const stdoutLines = stdout.split('\n');
|
||||
|
||||
for (const [idx, line] of stdoutLines.entries()) {
|
||||
if (line.startsWith('data:image/png;base64')) {
|
||||
if (files) {
|
||||
files.push({
|
||||
type: 'image/png',
|
||||
data: line
|
||||
});
|
||||
} else {
|
||||
files = [
|
||||
{
|
||||
type: 'image/png',
|
||||
data: line
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (stdout.startsWith(`${line}\n`)) {
|
||||
stdout = stdout.replace(`${line}\n`, ``);
|
||||
} else if (stdout.startsWith(`${line}`)) {
|
||||
stdout = stdout.replace(`${line}`, ``);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data['result']) {
|
||||
result = data['result'];
|
||||
const resultLines = result.split('\n');
|
||||
|
||||
for (const [idx, line] of resultLines.entries()) {
|
||||
if (line.startsWith('data:image/png;base64')) {
|
||||
if (files) {
|
||||
files.push({
|
||||
type: 'image/png',
|
||||
data: line
|
||||
});
|
||||
} else {
|
||||
files = [
|
||||
{
|
||||
type: 'image/png',
|
||||
data: line
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (result.startsWith(`${line}\n`)) {
|
||||
result = result.replace(`${line}\n`, ``);
|
||||
} else if (result.startsWith(`${line}`)) {
|
||||
result = result.replace(`${line}`, ``);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data['stderr'] && (stderr = data['stderr']);
|
||||
data['result'] && (result = data['result']);
|
||||
|
||||
@@ -277,13 +345,43 @@ __builtins__.input = input`);
|
||||
render();
|
||||
}
|
||||
|
||||
$: dispatch('code', { lang, code });
|
||||
$: onCode({ lang, code });
|
||||
|
||||
$: if (attributes) {
|
||||
onAttributesUpdate();
|
||||
}
|
||||
|
||||
const onAttributesUpdate = () => {
|
||||
if (attributes?.output) {
|
||||
// Create a helper function to unescape HTML entities
|
||||
const unescapeHtml = (html) => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = html;
|
||||
return textArea.value;
|
||||
};
|
||||
|
||||
try {
|
||||
// Unescape the HTML-encoded string
|
||||
const unescapedOutput = unescapeHtml(attributes.output);
|
||||
|
||||
// Parse the unescaped string into JSON
|
||||
const output = JSON.parse(unescapedOutput);
|
||||
|
||||
// Assign the parsed values to variables
|
||||
stdout = output.stdout;
|
||||
stderr = output.stderr;
|
||||
result = output.result;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
console.log('codeblock', lang, code);
|
||||
|
||||
if (lang) {
|
||||
dispatch('code', { lang, code });
|
||||
onCode({ lang, code });
|
||||
}
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
mermaid.initialize({
|
||||
@@ -299,6 +397,12 @@ __builtins__.input = input`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (pyodideWorker) {
|
||||
pyodideWorker.terminate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -306,7 +410,7 @@ __builtins__.input = input`);
|
||||
{#if lang === 'mermaid'}
|
||||
{#if mermaidHtml}
|
||||
<SvgPanZoom
|
||||
className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
|
||||
className=" border border-gray-100 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
|
||||
svg={mermaidHtml}
|
||||
content={_token.text}
|
||||
/>
|
||||
@@ -322,18 +426,39 @@ __builtins__.input = input`);
|
||||
class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 translate-y-[1px]">
|
||||
<button
|
||||
class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||
on:click={collapseCodeBlock}
|
||||
>
|
||||
<div>
|
||||
<ChevronUpDown className="size-3" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{collapsed ? $i18n.t('Expand') : $i18n.t('Collapse')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
|
||||
{#if executing}
|
||||
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||
{:else if run}
|
||||
<button
|
||||
class="run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||
on:click={async () => {
|
||||
code = _code;
|
||||
await tick();
|
||||
executePython(code);
|
||||
}}>{$i18n.t('Run')}</button
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<CommandLine className="size-3" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{$i18n.t('Run')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -361,34 +486,80 @@ __builtins__.input = input`);
|
||||
: 'rounded-b-lg'} overflow-hidden"
|
||||
>
|
||||
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
|
||||
<CodeEditor
|
||||
value={code}
|
||||
{id}
|
||||
{lang}
|
||||
on:save={() => {
|
||||
saveCode();
|
||||
}}
|
||||
on:change={(e) => {
|
||||
_code = e.detail.value;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if !collapsed}
|
||||
<CodeEditor
|
||||
value={code}
|
||||
{id}
|
||||
{lang}
|
||||
onSave={() => {
|
||||
saveCode();
|
||||
}}
|
||||
onChange={(value) => {
|
||||
_code = value;
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-lg! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"
|
||||
>
|
||||
<span class="text-gray-500 italic">
|
||||
{$i18n.t('{{COUNT}} hidden lines', {
|
||||
COUNT: code.split('\n').length
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="plt-canvas-{id}"
|
||||
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||
/>
|
||||
{#if !collapsed}
|
||||
<div
|
||||
id="plt-canvas-{id}"
|
||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||
/>
|
||||
|
||||
{#if executing}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">Running...</div>
|
||||
</div>
|
||||
{:else if stdout || stderr || result}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">{stdout || stderr || result}</div>
|
||||
</div>
|
||||
{#if executing || stdout || stderr || result || files}
|
||||
<div
|
||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
|
||||
>
|
||||
{#if executing}
|
||||
<div class=" ">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">Running...</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if stdout || stderr}
|
||||
<div class=" ">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div
|
||||
class="text-sm {stdout?.split('\n')?.length > 100
|
||||
? `max-h-96`
|
||||
: ''} overflow-y-auto"
|
||||
>
|
||||
{stdout || stderr}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if result || files}
|
||||
<div class=" ">
|
||||
<div class=" text-gray-500 text-xs mb-1">RESULT</div>
|
||||
{#if result}
|
||||
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
|
||||
{/if}
|
||||
{#if files}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each files as file}
|
||||
{#if file.type.startsWith('image')}
|
||||
<img src={file.data} alt="Output" class=" w-full max-w-[36rem]" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
{/if}
|
||||
{#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0}
|
||||
<div class="flex flex-col w-full">
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
||||
<div class=" text-sm font-medium dark:text-gray-300">
|
||||
{$i18n.t('Files')}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
export let floatingButtons = true;
|
||||
|
||||
export let onSourceClick = () => {};
|
||||
export let onTaskClick = () => {};
|
||||
|
||||
export let onAddMessages = () => {};
|
||||
|
||||
let contentContainerElement;
|
||||
@@ -118,6 +120,11 @@
|
||||
sourceIds={(sources ?? []).reduce((acc, s) => {
|
||||
let ids = [];
|
||||
s.document.forEach((document, index) => {
|
||||
if (model?.info?.meta?.capabilities?.citations == false) {
|
||||
ids.push('N/A');
|
||||
return ids;
|
||||
}
|
||||
|
||||
const metadata = s.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
|
||||
@@ -141,6 +148,7 @@
|
||||
return acc.filter((item, index) => acc.indexOf(item) === index);
|
||||
}, [])}
|
||||
{onSourceClick}
|
||||
{onTaskClick}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
export let save = false;
|
||||
|
||||
export let sourceIds = [];
|
||||
|
||||
export let onSourceClick = () => {};
|
||||
export let onTaskClick = () => {};
|
||||
|
||||
let tokens = [];
|
||||
|
||||
@@ -42,6 +44,7 @@
|
||||
{tokens}
|
||||
{id}
|
||||
{save}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||
{@html `${token.text}`}
|
||||
{:else if token.text.includes(`<source_id`)}
|
||||
<Source {token} onClick={onSourceClick} />
|
||||
<Source {id} {token} onClick={onSourceClick} />
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
@@ -44,13 +44,9 @@
|
||||
{:else if token.type === 'image'}
|
||||
<Image src={token.href} alt={token.text} />
|
||||
{:else if token.type === 'strong'}
|
||||
<strong>
|
||||
<svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} />
|
||||
</strong>
|
||||
<strong><svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} /></strong>
|
||||
{:else if token.type === 'em'}
|
||||
<em>
|
||||
<svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} />
|
||||
</em>
|
||||
<em><svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} /></em>
|
||||
{:else if token.type === 'codespan'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
@@ -64,9 +60,7 @@
|
||||
{:else if token.type === 'br'}
|
||||
<br />
|
||||
{:else if token.type === 'del'}
|
||||
<del>
|
||||
<svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} />
|
||||
</del>
|
||||
<del><svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} /></del>
|
||||
{:else if token.type === 'inlineKatex'}
|
||||
{#if token.text}
|
||||
<KatexRenderer content={token.text} displayMode={false} />
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||
import Source from './Source.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
export let top = true;
|
||||
export let attributes = {};
|
||||
|
||||
export let save = false;
|
||||
|
||||
export let onTaskClick: Function = () => {};
|
||||
export let onSourceClick: Function = () => {};
|
||||
|
||||
const headerComponent = (depth: number) => {
|
||||
@@ -71,9 +75,9 @@
|
||||
<!-- {JSON.stringify(tokens)} -->
|
||||
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||
{#if token.type === 'hr'}
|
||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
{:else if token.type === 'heading'}
|
||||
<svelte:element this={headerComponent(token.depth)}>
|
||||
<svelte:element this={headerComponent(token.depth)} dir="auto">
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
|
||||
</svelte:element>
|
||||
{:else if token.type === 'code'}
|
||||
@@ -83,15 +87,16 @@
|
||||
{token}
|
||||
lang={token?.lang ?? ''}
|
||||
code={token?.text ?? ''}
|
||||
{attributes}
|
||||
{save}
|
||||
on:code={(e) => {
|
||||
dispatch('code', e.detail);
|
||||
onCode={(value) => {
|
||||
dispatch('code', value);
|
||||
}}
|
||||
on:save={(e) => {
|
||||
onSave={(value) => {
|
||||
dispatch('update', {
|
||||
raw: token.raw,
|
||||
oldContent: token.text,
|
||||
newContent: e.detail
|
||||
newContent: value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -111,11 +116,11 @@
|
||||
{#each token.header as header, headerIdx}
|
||||
<th
|
||||
scope="col"
|
||||
class="!px-3 !py-1.5 cursor-pointer border border-gray-50 dark:border-gray-850"
|
||||
class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
|
||||
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 text-left">
|
||||
<div class="flex-shrink-0 break-normal">
|
||||
<div class="shrink-0 break-normal">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
@@ -132,7 +137,7 @@
|
||||
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td
|
||||
class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850"
|
||||
class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
|
||||
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col break-normal">
|
||||
@@ -165,18 +170,38 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if token.type === 'blockquote'}
|
||||
<blockquote>
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
|
||||
<blockquote dir="auto">
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
|
||||
</blockquote>
|
||||
{:else if token.type === 'list'}
|
||||
{#if token.ordered}
|
||||
<ol start={token.start || 1}>
|
||||
{#each token.items as item, itemIdx}
|
||||
<li>
|
||||
<li dir="auto" class="text-start">
|
||||
{#if item?.task}
|
||||
<input
|
||||
class=" translate-y-[1px] -translate-x-1"
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
on:change={(e) => {
|
||||
onTaskClick({
|
||||
id: id,
|
||||
token: token,
|
||||
tokenIdx: tokenIdx,
|
||||
item: item,
|
||||
itemIdx: itemIdx,
|
||||
checked: e.target.checked
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -184,20 +209,51 @@
|
||||
{:else}
|
||||
<ul>
|
||||
{#each token.items as item, itemIdx}
|
||||
<li>
|
||||
<li dir="auto" class="text-start">
|
||||
{#if item?.task}
|
||||
<input
|
||||
class=" translate-y-[1px] -translate-x-1"
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
on:change={(e) => {
|
||||
onTaskClick({
|
||||
id: id,
|
||||
token: token,
|
||||
tokenIdx: tokenIdx,
|
||||
item: item,
|
||||
itemIdx: itemIdx,
|
||||
checked: e.target.checked
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if token.type === 'details'}
|
||||
<Collapsible title={token.summary} attributes={token?.attributes} className="w-fit space-y-1">
|
||||
<Collapsible
|
||||
title={token.summary}
|
||||
attributes={token?.attributes}
|
||||
className="w-full space-y-1"
|
||||
dir="auto"
|
||||
>
|
||||
<div class=" mb-1.5" slot="content">
|
||||
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-d`}
|
||||
tokens={marked.lexer(token.text)}
|
||||
attributes={token?.attributes}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
{:else if token.type === 'html'}
|
||||
@@ -206,6 +262,8 @@
|
||||
{@html html}
|
||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||
{@html `${token.text}`}
|
||||
{:else if token.text.includes(`<source_id`)}
|
||||
<Source {id} {token} onClick={onSourceClick} />
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
@@ -218,7 +276,7 @@
|
||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
||||
></iframe>
|
||||
{:else if token.type === 'paragraph'}
|
||||
<p>
|
||||
<p dir="auto">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-p`}
|
||||
tokens={token.tokens ?? []}
|
||||
@@ -227,7 +285,7 @@
|
||||
</p>
|
||||
{:else if token.type === 'text'}
|
||||
{#if top}
|
||||
<p>
|
||||
<p dir="auto">
|
||||
{#if token.tokens}
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
|
||||
{:else}
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
<script lang="ts">
|
||||
export let id;
|
||||
export let token;
|
||||
export let onClick: Function = () => {};
|
||||
|
||||
let id = '';
|
||||
function extractDataAttribute(input) {
|
||||
// Use a regular expression to extract the value of the `data` attribute
|
||||
const match = input.match(/data="([^"]*)"/);
|
||||
// Check if a match was found and return the first captured group
|
||||
return match ? match[1] : null;
|
||||
let attributes: Record<string, string | undefined> = {};
|
||||
|
||||
function extractAttributes(input: string): Record<string, string> {
|
||||
const regex = /(\w+)="([^"]*)"/g;
|
||||
let match;
|
||||
let attrs: Record<string, string> = {};
|
||||
|
||||
// Loop through all matches and populate the attributes object
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
attrs[match[1]] = match[2];
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
$: id = extractDataAttribute(token.text);
|
||||
// Helper function to return only the domain from a URL
|
||||
function getDomain(url: string): string {
|
||||
const domain = url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0];
|
||||
return domain;
|
||||
}
|
||||
|
||||
// Helper function to check if text is a URL and return the domain
|
||||
function formattedTitle(title: string): string {
|
||||
if (title.startsWith('http')) {
|
||||
return getDomain(title);
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
$: attributes = extractAttributes(token.text);
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg"
|
||||
on:click={() => {
|
||||
onClick(id);
|
||||
}}
|
||||
>
|
||||
<span class="line-clamp-1">
|
||||
{id}
|
||||
</span>
|
||||
</button>
|
||||
{#if attributes.title !== 'N/A'}
|
||||
<button
|
||||
class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg"
|
||||
on:click={() => {
|
||||
onClick(id, attributes.data);
|
||||
}}
|
||||
>
|
||||
<span class="line-clamp-1">
|
||||
{attributes.title ? formattedTitle(attributes.title) : ''}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{deleteMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
{addMessages}
|
||||
@@ -95,6 +96,7 @@
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{deleteMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
{mergeResponses}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Name from './Name.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let chatId;
|
||||
export let history;
|
||||
@@ -32,6 +34,8 @@
|
||||
export let actionMessage: Function;
|
||||
|
||||
export let submitMessage: Function;
|
||||
export let deleteMessage: Function;
|
||||
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
@@ -190,10 +194,10 @@
|
||||
<div
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
||||
?.modelIdx == modelIdx
|
||||
? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
|
||||
? `border-gray-100 dark:border-gray-850 border-[1.5px] ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`
|
||||
: `border-gray-50 dark:border-gray-850 border-dashed ${
|
||||
: `border-gray-100 dark:border-gray-850 border-dashed ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`} transition-all p-5 rounded-2xl"
|
||||
on:click={async () => {
|
||||
@@ -226,6 +230,7 @@
|
||||
{editMessage}
|
||||
{saveMessage}
|
||||
{rateMessage}
|
||||
{deleteMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{continueResponse}
|
||||
@@ -264,7 +269,7 @@
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
{dayjs(message.timestamp * 1000).format('LT')}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
@@ -281,7 +286,7 @@
|
||||
</div>
|
||||
|
||||
{#if isLastMessage}
|
||||
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850"
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-100 dark:border-gray-850"
|
||||
id="message-feedback-{message.id}"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -142,10 +142,10 @@
|
||||
<!-- 1-10 scale -->
|
||||
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
|
||||
<button
|
||||
class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
|
||||
class="size-7 text-sm border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
|
||||
rating
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white disabled:dark:bg-gray-900"
|
||||
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white dark:disabled:bg-gray-900"
|
||||
on:click={() => {
|
||||
detailedRating = rating;
|
||||
}}
|
||||
@@ -175,7 +175,7 @@
|
||||
<div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
|
||||
{#each reasons as reason}
|
||||
<button
|
||||
class="px-3 py-0.5 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
|
||||
class="px-3 py-0.5 border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
|
||||
reason
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} transition rounded-xl"
|
||||
@@ -223,7 +223,7 @@
|
||||
<div class="mt-2">
|
||||
<textarea
|
||||
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-hidden resize-none rounded-xl"
|
||||
placeholder={$i18n.t('Feel free to add specific details')}
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { config, models, settings, user } from '$lib/stores';
|
||||
import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations';
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
import { generateTags } from '$lib/apis';
|
||||
|
||||
import { config, models, settings, temporaryChatEnabled, TTSWorker, user } from '$lib/stores';
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import {
|
||||
@@ -31,16 +37,14 @@
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
|
||||
import Sparkles from '$lib/components/icons/Sparkles.svelte';
|
||||
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import Error from './Error.svelte';
|
||||
import Citations from './Citations.svelte';
|
||||
import CodeExecutions from './CodeExecutions.svelte';
|
||||
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import ContentRenderer from './ContentRenderer.svelte';
|
||||
import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations';
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
import { generateTags } from '$lib/apis';
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
|
||||
interface MessageType {
|
||||
id: string;
|
||||
@@ -114,6 +118,7 @@
|
||||
export let saveMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
export let actionMessage: Function;
|
||||
export let deleteMessage: Function;
|
||||
|
||||
export let submitMessage: Function;
|
||||
export let continueResponse: Function;
|
||||
@@ -124,6 +129,9 @@
|
||||
export let isLastMessage = true;
|
||||
export let readOnly = false;
|
||||
|
||||
let buttonsContainerElement: HTMLDivElement;
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
let model = null;
|
||||
$: model = $models.find((m) => m.id === message.model);
|
||||
|
||||
@@ -192,62 +200,7 @@
|
||||
|
||||
speaking = true;
|
||||
|
||||
if ($config.audio.tts.engine !== '') {
|
||||
loadingSpeech = true;
|
||||
|
||||
const messageContentParts: string[] = getMessageContentParts(
|
||||
message.content,
|
||||
$config?.audio?.tts?.split_on ?? 'punctuation'
|
||||
);
|
||||
|
||||
if (!messageContentParts.length) {
|
||||
console.log('No content to speak');
|
||||
toast.info($i18n.t('No content to speak'));
|
||||
|
||||
speaking = false;
|
||||
loadingSpeech = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Prepared message content for TTS', messageContentParts);
|
||||
|
||||
audioParts = messageContentParts.reduce(
|
||||
(acc, _sentence, idx) => {
|
||||
acc[idx] = null;
|
||||
return acc;
|
||||
},
|
||||
{} as typeof audioParts
|
||||
);
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
|
||||
for (const [idx, sentence] of messageContentParts.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
|
||||
? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
: $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(`${error}`);
|
||||
|
||||
speaking = false;
|
||||
loadingSpeech = false;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
audioParts[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($config.audio.tts.engine === '') {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(() => {
|
||||
voices = speechSynthesis.getVoices();
|
||||
@@ -282,6 +235,97 @@
|
||||
speechSynthesis.speak(speak);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
loadingSpeech = true;
|
||||
|
||||
const messageContentParts: string[] = getMessageContentParts(
|
||||
message.content,
|
||||
$config?.audio?.tts?.split_on ?? 'punctuation'
|
||||
);
|
||||
|
||||
if (!messageContentParts.length) {
|
||||
console.log('No content to speak');
|
||||
toast.info($i18n.t('No content to speak'));
|
||||
|
||||
speaking = false;
|
||||
loadingSpeech = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Prepared message content for TTS', messageContentParts);
|
||||
|
||||
audioParts = messageContentParts.reduce(
|
||||
(acc, _sentence, idx) => {
|
||||
acc[idx] = null;
|
||||
return acc;
|
||||
},
|
||||
{} as typeof audioParts
|
||||
);
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
|
||||
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
||||
if (!$TTSWorker) {
|
||||
await TTSWorker.set(
|
||||
new KokoroWorker({
|
||||
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
|
||||
})
|
||||
);
|
||||
|
||||
await $TTSWorker.init();
|
||||
}
|
||||
|
||||
for (const [idx, sentence] of messageContentParts.entries()) {
|
||||
const blob = await $TTSWorker
|
||||
.generate({
|
||||
text: sentence,
|
||||
voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(`${error}`);
|
||||
|
||||
speaking = false;
|
||||
loadingSpeech = false;
|
||||
});
|
||||
|
||||
if (blob) {
|
||||
const audio = new Audio(blob);
|
||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
audioParts[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [idx, sentence] of messageContentParts.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
|
||||
? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
: $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(`${error}`);
|
||||
|
||||
speaking = false;
|
||||
loadingSpeech = false;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
audioParts[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -461,6 +505,10 @@
|
||||
feedbackLoading = false;
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async () => {
|
||||
deleteMessage(message.id);
|
||||
};
|
||||
|
||||
$: if (!edit) {
|
||||
(async () => {
|
||||
await tick();
|
||||
@@ -471,16 +519,36 @@
|
||||
// console.log('ResponseMessage mounted');
|
||||
|
||||
await tick();
|
||||
if (buttonsContainerElement) {
|
||||
console.log(buttonsContainerElement);
|
||||
buttonsContainerElement.addEventListener('wheel', function (event) {
|
||||
// console.log(event.deltaY);
|
||||
|
||||
event.preventDefault();
|
||||
if (event.deltaY !== 0) {
|
||||
// Adjust horizontal scroll position based on vertical scroll
|
||||
buttonsContainerElement.scrollLeft += event.deltaY;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete message?')}
|
||||
on:confirm={() => {
|
||||
deleteMessageHandler();
|
||||
}}
|
||||
/>
|
||||
|
||||
{#key message.id}
|
||||
<div
|
||||
class=" flex w-full message-{message.id}"
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
@@ -500,7 +568,7 @@
|
||||
<div
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -603,7 +671,7 @@
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={editTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
class=" bg-transparent outline-hidden w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
@@ -674,12 +742,32 @@
|
||||
floatingButtons={message?.done}
|
||||
save={!readOnly}
|
||||
{model}
|
||||
onSourceClick={(e) => {
|
||||
onTaskClick={async (e) => {
|
||||
console.log(e);
|
||||
const sourceButton = document.getElementById(`source-${e}`);
|
||||
}}
|
||||
onSourceClick={async (id, idx) => {
|
||||
console.log(id, idx);
|
||||
let sourceButton = document.getElementById(`source-${message.id}-${idx}`);
|
||||
const sourcesCollapsible = document.getElementById(`collapsible-sources`);
|
||||
|
||||
if (sourceButton) {
|
||||
sourceButton.click();
|
||||
} else if (sourcesCollapsible) {
|
||||
// Open sources collapsible so we can click the source button
|
||||
sourcesCollapsible
|
||||
.querySelector('div:first-child')
|
||||
.dispatchEvent(new PointerEvent('pointerup', {}));
|
||||
|
||||
// Wait for next frame to ensure DOM updates
|
||||
await new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
});
|
||||
|
||||
// Try clicking the source button again
|
||||
sourceButton = document.getElementById(`source-${message.id}-${idx}`);
|
||||
sourceButton && sourceButton.click();
|
||||
}
|
||||
}}
|
||||
onAddMessages={({ modelId, parentId, messages }) => {
|
||||
@@ -715,7 +803,7 @@
|
||||
{/if}
|
||||
|
||||
{#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
|
||||
<Citations sources={message?.sources ?? message?.citations} />
|
||||
<Citations id={message?.id} sources={message?.sources ?? message?.citations} />
|
||||
{/if}
|
||||
|
||||
{#if message.code_executions}
|
||||
@@ -727,10 +815,11 @@
|
||||
</div>
|
||||
|
||||
{#if !edit}
|
||||
{#if message.done || siblings.length > 1}
|
||||
<div
|
||||
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500 mt-0.5"
|
||||
>
|
||||
<div
|
||||
bind:this={buttonsContainerElement}
|
||||
class="flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500 mt-0.5"
|
||||
>
|
||||
{#if message.done || siblings.length > 1}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center min-w-fit" dir="ltr">
|
||||
<button
|
||||
@@ -1027,7 +1116,7 @@
|
||||
{/if}
|
||||
|
||||
{#if !readOnly}
|
||||
{#if $config?.features.enable_message_rating ?? true}
|
||||
{#if !$temporaryChatEnabled && ($config?.features.enable_message_rating ?? true)}
|
||||
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
@@ -1176,6 +1265,36 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if siblings.length > 1}
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
id="continue-response-button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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>
|
||||
{/if}
|
||||
|
||||
{#if isLastMessage}
|
||||
{#each model?.actions ?? [] as action}
|
||||
<Tooltip content={action.name} placement="bottom">
|
||||
@@ -1208,8 +1327,8 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if message.done && showRateComment}
|
||||
<RateComment
|
||||
|
||||
@@ -5,31 +5,41 @@
|
||||
<div class="w-full mt-2 mb-2">
|
||||
<div class="animate-pulse flex w-full">
|
||||
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
|
||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded mr-14" />
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4"
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1 mr-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded" />
|
||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let user;
|
||||
|
||||
@@ -32,6 +36,8 @@
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||
@@ -83,9 +89,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete message?')}
|
||||
on:confirm={() => {
|
||||
deleteMessageHandler();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
@@ -112,7 +126,7 @@
|
||||
<div
|
||||
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -143,149 +157,177 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if edit === true}
|
||||
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
|
||||
<div class="max-h-96 overflow-auto">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
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('confirm-edit-message-button')?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
|
||||
<div>
|
||||
<button
|
||||
id="save-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler(false);
|
||||
{#if message.content !== ''}
|
||||
{#if edit === true}
|
||||
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
|
||||
<div class="max-h-96 overflow-auto">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-hidden w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
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('confirm-edit-message-button')?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="confirm-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
|
||||
<div
|
||||
class="rounded-3xl {($settings?.chatBubble ?? true)
|
||||
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
|
||||
message.files ? 'rounded-tr-lg' : ''
|
||||
}`
|
||||
: ' w-full'}"
|
||||
>
|
||||
{#if message.content}
|
||||
<Markdown id={message.id} content={message.content} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" flex {($settings?.chatBubble ?? true)
|
||||
? 'justify-end'
|
||||
: ''} text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !readOnly}
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
|
||||
<div>
|
||||
<button
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
id="save-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
editMessageConfirmHandler(false);
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="confirm-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
|
||||
<div
|
||||
class="rounded-3xl {($settings?.chatBubble ?? true)
|
||||
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
|
||||
message.files ? 'rounded-tr-lg' : ''
|
||||
}`
|
||||
: ' w-full'}"
|
||||
>
|
||||
{#if message.content}
|
||||
<Markdown id={message.id} content={message.content} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" flex {($settings?.chatBubble ?? true)
|
||||
? 'justify-end'
|
||||
: ''} text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
|
||||
>
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !readOnly}
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -299,118 +341,96 @@
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !isFirstMessage && !readOnly}
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
deleteMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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>
|
||||
{/if}
|
||||
|
||||
{#if $settings?.chatBubble ?? true}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
{#if !isFirstMessage && !readOnly}
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
class="invisible group-hover:visible p-1 rounded-sm dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
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>
|
||||
|
||||
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $settings?.chatBubble ?? true}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
|
||||
>
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,15 @@
|
||||
|
||||
import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||
|
||||
import { user, MODEL_DOWNLOAD_POOL, models, mobile, temporaryChatEnabled } from '$lib/stores';
|
||||
import {
|
||||
user,
|
||||
MODEL_DOWNLOAD_POOL,
|
||||
models,
|
||||
mobile,
|
||||
temporaryChatEnabled,
|
||||
settings,
|
||||
config
|
||||
} from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
|
||||
import { getModels } from '$lib/apis';
|
||||
@@ -44,12 +52,17 @@
|
||||
export let className = 'w-[32rem]';
|
||||
export let triggerClassName = 'text-lg';
|
||||
|
||||
let tagsContainerElement;
|
||||
|
||||
let show = false;
|
||||
let tags = [];
|
||||
|
||||
let selectedModel = '';
|
||||
$: selectedModel = items.find((item) => item.value === value) ?? '';
|
||||
|
||||
let searchValue = '';
|
||||
let selectedTag = '';
|
||||
|
||||
let ollamaVersion = null;
|
||||
|
||||
let selectedModelIdx = 0;
|
||||
@@ -71,10 +84,23 @@
|
||||
);
|
||||
|
||||
$: filteredItems = searchValue
|
||||
? fuse.search(searchValue).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: items;
|
||||
? fuse
|
||||
.search(searchValue)
|
||||
.map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
.filter((item) => {
|
||||
if (selectedTag === '') {
|
||||
return true;
|
||||
}
|
||||
return item.model?.info?.meta?.tags?.map((tag) => tag.name).includes(selectedTag);
|
||||
})
|
||||
: items.filter((item) => {
|
||||
if (selectedTag === '') {
|
||||
return true;
|
||||
}
|
||||
return item.model?.info?.meta?.tags?.map((tag) => tag.name).includes(selectedTag);
|
||||
});
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||
@@ -186,7 +212,12 @@
|
||||
})
|
||||
);
|
||||
|
||||
models.set(await getModels(localStorage.token));
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
toast.error($i18n.t('Download canceled'));
|
||||
}
|
||||
@@ -201,6 +232,13 @@
|
||||
|
||||
onMount(async () => {
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
|
||||
if (items) {
|
||||
tags = items.flatMap((item) => item.model?.info?.meta?.tags ?? []).map((tag) => tag.name);
|
||||
|
||||
// Remove duplicates and sort
|
||||
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
});
|
||||
|
||||
const cancelModelPullHandler = async (model: string) => {
|
||||
@@ -235,7 +273,7 @@
|
||||
id="model-selector-{id}-button"
|
||||
>
|
||||
<div
|
||||
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-none"
|
||||
class="flex w-full text-left px-0.5 outline-hidden bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-hidden"
|
||||
>
|
||||
{#if selectedModel}
|
||||
{selectedModel.label}
|
||||
@@ -249,20 +287,20 @@
|
||||
<DropdownMenu.Content
|
||||
class=" z-40 {$mobile
|
||||
? `w-full`
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-none"
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
|
||||
transition={flyAndScale}
|
||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||
sideOffset={3}
|
||||
>
|
||||
<slot>
|
||||
{#if searchEnabled}
|
||||
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
|
||||
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-1.5">
|
||||
<Search className="size-4" strokeWidth="2.5" />
|
||||
|
||||
<input
|
||||
id="model-search-input"
|
||||
bind:value={searchValue}
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
placeholder={searchPlaceholder}
|
||||
autocomplete="off"
|
||||
on:keydown={(e) => {
|
||||
@@ -284,15 +322,46 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
||||
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
|
||||
<div class="px-3 mb-2 max-h-64 overflow-y-auto scrollbar-hidden group relative">
|
||||
{#if tags}
|
||||
<div class=" flex w-full sticky">
|
||||
<div
|
||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
|
||||
bind:this={tagsContainerElement}
|
||||
>
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === ''
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedTag = '';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('All')}
|
||||
</button>
|
||||
|
||||
{#each tags as tag}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === tag
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedTag = tag;
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each filteredItems as item, index}
|
||||
<button
|
||||
aria-label="model-item"
|
||||
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index ===
|
||||
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
|
||||
selectedModelIdx
|
||||
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
||||
: ''}"
|
||||
@@ -309,7 +378,7 @@
|
||||
<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
@@ -358,7 +427,24 @@
|
||||
|
||||
<!-- {JSON.stringify(item.info)} -->
|
||||
|
||||
{#if item.model.owned_by === 'openai'}
|
||||
{#if item.model?.direct}
|
||||
<Tooltip content={`${'Direct'}`}>
|
||||
<div class="translate-y-[1px]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else if item.model.owned_by === 'openai'}
|
||||
<Tooltip content={`${'External'}`}>
|
||||
<div class="translate-y-[1px]">
|
||||
<svg
|
||||
@@ -411,11 +497,13 @@
|
||||
{/if}
|
||||
|
||||
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
|
||||
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
|
||||
<div
|
||||
class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
|
||||
>
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
<Tooltip content={tag.name}>
|
||||
<Tooltip content={tag.name} className="flex-shrink-0">
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
@@ -448,7 +536,7 @@
|
||||
placement="top-start"
|
||||
>
|
||||
<button
|
||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
@@ -462,7 +550,7 @@
|
||||
|
||||
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||
<div
|
||||
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-lg cursor-pointer data-highlighted:bg-muted"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||
@@ -497,7 +585,7 @@
|
||||
Downloading "{model}"
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||
: ''}
|
||||
@@ -545,11 +633,11 @@
|
||||
</div>
|
||||
|
||||
{#if showTemporaryChatControl}
|
||||
<hr class="border-gray-50 dark:border-gray-800" />
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
|
||||
<div class="flex items-center mx-2 my-2">
|
||||
<button
|
||||
class="flex justify-between w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 px-3 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
class="flex justify-between w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 px-3 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
|
||||
on:click={async () => {
|
||||
temporaryChatEnabled.set(!$temporaryChatEnabled);
|
||||
await goto('/');
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
|
||||
<div
|
||||
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
|
||||
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||
></div>
|
||||
|
||||
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
|
||||
@@ -114,7 +114,7 @@
|
||||
</div>
|
||||
</button>
|
||||
</Menu>
|
||||
{:else if $mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
|
||||
{:else if $mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
@@ -130,7 +130,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !$mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
|
||||
{#if !$mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
export let selectedToolIds = [];
|
||||
export let imageGenerationEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
let models = [];
|
||||
@@ -87,7 +88,7 @@
|
||||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
|
||||
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content="This chat won't appear in history and your messages will not be saved."
|
||||
@@ -101,11 +102,11 @@
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
|
||||
>
|
||||
<div class="w-full flex flex-col justify-center items-center">
|
||||
<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex flex-shrink-0 justify-center">
|
||||
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex shrink-0 justify-center">
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
|
||||
{#each models as model, modelIdx}
|
||||
<Tooltip
|
||||
@@ -125,7 +126,7 @@
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -135,7 +136,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
<div class=" text-3xl @sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
{#if models[selectedModelIdx]?.name}
|
||||
{models[selectedModelIdx]?.name}
|
||||
{:else}
|
||||
@@ -183,11 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
|
||||
? 'mt-2'
|
||||
: ''}"
|
||||
>
|
||||
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
|
||||
<MessageInput
|
||||
{history}
|
||||
{selectedModels}
|
||||
@@ -196,6 +193,7 @@
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
{transparentBackground}
|
||||
@@ -215,9 +213,11 @@
|
||||
<div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}>
|
||||
<div class="mx-5">
|
||||
<Suggestions
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
|
||||
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
inputValue={prompt}
|
||||
on:select={(e) => {
|
||||
selectSuggestionPrompt(e.detail);
|
||||
}}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
{#if ollamaVersion}
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||
@@ -104,7 +104,41 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
{#if $config?.license_metadata}
|
||||
<div class="mb-2 text-xs">
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||
{/if}
|
||||
|
||||
<span class=" capitalize">{$config?.license_metadata?.type}</span> license purchased by
|
||||
<span class=" capitalize">{$config?.license_metadata?.organization_name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
<img
|
||||
alt="Discord"
|
||||
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Emoji graphics provided by
|
||||
@@ -112,29 +146,6 @@
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC-BY 4.0</a>.
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
<img
|
||||
alt="Discord"
|
||||
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<pre
|
||||
class="text-xs text-gray-400 dark:text-gray-500">Copyright (c) {new Date().getFullYear()} <a
|
||||
@@ -172,9 +183,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||
{/if}
|
||||
{$i18n.t('Created by')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { user, config, settings } from '$lib/stores';
|
||||
import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
|
||||
import { updateUserProfile, createAPIKey, getAPIKey, getSessionUser } from '$lib/apis/auths';
|
||||
|
||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||
import { getGravatarUrl } from '$lib/apis/utils';
|
||||
@@ -53,7 +53,13 @@
|
||||
);
|
||||
|
||||
if (updatedUser) {
|
||||
await user.set(updatedUser);
|
||||
// Get Session User Info
|
||||
const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
await user.set(sessionUser);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -208,7 +214,7 @@
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl($user.email);
|
||||
const url = await getGravatarUrl(localStorage.token, $user.email);
|
||||
|
||||
profileImageUrl = url;
|
||||
}}>{$i18n.t('Use Gravatar')}</button
|
||||
@@ -230,7 +236,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"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
required
|
||||
@@ -245,7 +251,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"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="url"
|
||||
placeholder={$i18n.t('Enter your webhook URL')}
|
||||
bind:value={webhookUrl}
|
||||
@@ -260,7 +266,7 @@
|
||||
<UpdatePassword />
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-4" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-4" />
|
||||
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full bg-transparent dark:text-gray-300 outline-none placeholder:opacity-30"
|
||||
class="w-full bg-transparent dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
type="password"
|
||||
bind:value={currentPassword}
|
||||
placeholder={$i18n.t('Enter your current password')}
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-none placeholder:opacity-30"
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
placeholder={$i18n.t('Enter your new password')}
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-none placeholder:opacity-30"
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
type="password"
|
||||
bind:value={newPasswordConfirm}
|
||||
placeholder={$i18n.t('Confirm your new password')}
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
export let params = {
|
||||
// Advanced
|
||||
stream_response: null, // Set stream responses for this model individually
|
||||
function_calling: null,
|
||||
seed: null,
|
||||
stop: null,
|
||||
temperature: null,
|
||||
reasoning_effort: null,
|
||||
logit_bias: null,
|
||||
frequency_penalty: null,
|
||||
repeat_last_n: null,
|
||||
mirostat: null,
|
||||
@@ -58,7 +60,7 @@
|
||||
{$i18n.t('Stream Chat Response')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
params.stream_response =
|
||||
(params?.stream_response ?? null) === null
|
||||
@@ -81,10 +83,39 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Function Calling')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
params.function_calling = (params?.function_calling ?? null) === null ? 'native' : null;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if params.function_calling === 'native'}
|
||||
<span class="ml-2 self-center">{$i18n.t('Native')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt. (Default: random)'
|
||||
'Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -95,7 +126,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.seed = (params?.seed ?? null) === null ? 0 : null;
|
||||
@@ -114,7 +145,7 @@
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<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"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Seed')}
|
||||
bind:value={params.seed}
|
||||
@@ -140,7 +171,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.stop = (params?.stop ?? null) === null ? '' : null;
|
||||
@@ -159,7 +190,7 @@
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter stop sequence')}
|
||||
bind:value={params.stop}
|
||||
@@ -173,7 +204,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The temperature of the model. Increasing the temperature will make the model answer more creatively. (Default: 0.8)'
|
||||
'The temperature of the model. Increasing the temperature will make the model answer more creatively.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -183,7 +214,7 @@
|
||||
{$i18n.t('Temperature')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
|
||||
@@ -205,7 +236,7 @@
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
max="2"
|
||||
step="0.05"
|
||||
bind:value={params.temperature}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
@@ -217,7 +248,7 @@
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
max="1"
|
||||
max="2"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
@@ -228,7 +259,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)'
|
||||
'Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -238,7 +269,7 @@
|
||||
{$i18n.t('Reasoning Effort')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.reasoning_effort = (params?.reasoning_effort ?? null) === null ? 'medium' : null;
|
||||
@@ -257,7 +288,7 @@
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter reasoning effort')}
|
||||
bind:value={params.reasoning_effort}
|
||||
@@ -271,17 +302,60 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Enable Mirostat sampling for controlling perplexity. (Default: 0, 0 = Disabled, 1 = Mirostat, 2 = Mirostat 2.0)'
|
||||
'Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Logit Bias')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.logit_bias = (params?.logit_bias ?? null) === null ? '' : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.logit_bias ?? null) === null}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if (params?.logit_bias ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg pl-2 py-2 px-1 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t(
|
||||
'Enter comma-seperated "token:bias_value" pairs (example: 5432:100, 413:-100)'
|
||||
)}
|
||||
bind:value={params.logit_bias}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t('Enable Mirostat sampling for controlling perplexity.')}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Mirostat')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
|
||||
@@ -326,7 +400,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Influences how quickly the algorithm responds to feedback from the generated text. A lower learning rate will result in slower adjustments, while a higher learning rate will make the algorithm more responsive. (Default: 0.1)'
|
||||
'Influences how quickly the algorithm responds to feedback from the generated text. A lower learning rate will result in slower adjustments, while a higher learning rate will make the algorithm more responsive.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -336,7 +410,7 @@
|
||||
{$i18n.t('Mirostat Eta')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
|
||||
@@ -381,7 +455,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Controls the balance between coherence and diversity of the output. A lower value will result in more focused and coherent text. (Default: 5.0)'
|
||||
'Controls the balance between coherence and diversity of the output. A lower value will result in more focused and coherent text.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -392,7 +466,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
|
||||
@@ -437,7 +511,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)'
|
||||
'Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -447,7 +521,7 @@
|
||||
{$i18n.t('Top K')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.top_k = (params?.top_k ?? null) === null ? 40 : null;
|
||||
@@ -469,7 +543,7 @@
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
max="1000"
|
||||
step="0.5"
|
||||
bind:value={params.top_k}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
@@ -492,7 +566,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)'
|
||||
'Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -503,7 +577,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
|
||||
@@ -548,7 +622,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)'
|
||||
'Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -558,7 +632,7 @@
|
||||
{$i18n.t('Min P')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.min_p = (params?.min_p ?? null) === null ? 0.0 : null;
|
||||
@@ -603,7 +677,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1)'
|
||||
'Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -614,7 +688,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
|
||||
@@ -635,7 +709,7 @@
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="0"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.05"
|
||||
bind:value={params.frequency_penalty}
|
||||
@@ -647,7 +721,7 @@
|
||||
bind:value={params.frequency_penalty}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="0"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="any"
|
||||
/>
|
||||
@@ -659,10 +733,120 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)'
|
||||
'Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Presence Penalty')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.presence_penalty = (params?.presence_penalty ?? null) === null ? 0.0 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.presence_penalty ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if (params?.presence_penalty ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.05"
|
||||
bind:value={params.presence_penalty}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
bind:value={params.presence_penalty}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Control the repetition of token sequences in the generated text. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 1.1) will be more lenient. At 1, it is disabled.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Repeat Penalty (Ollama)')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.repeat_penalty = (params?.repeat_penalty ?? null) === null ? 1.1 : null;
|
||||
}}
|
||||
>
|
||||
{#if (params?.repeat_penalty ?? null) === null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if (params?.repeat_penalty ?? null) !== null}
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.05"
|
||||
bind:value={params.repeat_penalty}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
bind:value={params.repeat_penalty}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t('Sets how far back for the model to look back to prevent repetition.')}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
@@ -670,7 +854,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
|
||||
@@ -715,7 +899,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)'
|
||||
'Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -726,7 +910,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
|
||||
@@ -770,9 +954,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Sets the size of the context window used to generate the next token. (Default: 2048)'
|
||||
)}
|
||||
content={$i18n.t('Sets the size of the context window used to generate the next token.')}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
>
|
||||
@@ -782,7 +964,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
|
||||
@@ -826,7 +1008,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The batch size determines how many text requests are processed together at once. A higher batch size can increase the performance and speed of the model, but it also requires more memory. (Default: 512)'
|
||||
'The batch size determines how many text requests are processed together at once. A higher batch size can increase the performance and speed of the model, but it also requires more memory.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -837,7 +1019,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_batch = (params?.num_batch ?? null) === null ? 512 : null;
|
||||
@@ -881,7 +1063,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'This option controls how many tokens are preserved when refreshing the context. For example, if set to 2, the last 2 tokens of the conversation context will be retained. Preserving context can help maintain the continuity of a conversation, but it may reduce the ability to respond to new topics. (Default: 24)'
|
||||
'This option controls how many tokens are preserved when refreshing the context. For example, if set to 2, the last 2 tokens of the conversation context will be retained. Preserving context can help maintain the continuity of a conversation, but it may reduce the ability to respond to new topics.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -892,7 +1074,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_keep = (params?.num_keep ?? null) === null ? 24 : null;
|
||||
@@ -936,7 +1118,7 @@
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'This option sets the maximum number of tokens the model can generate in its response. Increasing this limit allows the model to provide longer answers, but it may also increase the likelihood of unhelpful or irrelevant content being generated. (Default: 128)'
|
||||
'This option sets the maximum number of tokens the model can generate in its response. Increasing this limit allows the model to provide longer answers, but it may also increase the likelihood of unhelpful or irrelevant content being generated.'
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
@@ -947,7 +1129,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
|
||||
@@ -1002,7 +1184,7 @@
|
||||
{$i18n.t('use_mmap (Ollama)')}
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
|
||||
@@ -1043,7 +1225,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
|
||||
@@ -1085,7 +1267,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
|
||||
@@ -1141,7 +1323,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.num_gpu = (params?.num_gpu ?? null) === null ? 0 : null;
|
||||
@@ -1188,7 +1370,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.template = (params?.template ?? null) === null ? '' : null;
|
||||
@@ -1206,7 +1388,7 @@
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
|
||||
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-hidden rounded-lg -mb-1"
|
||||
placeholder={$i18n.t('Write your model template content here')}
|
||||
rows="4"
|
||||
bind:value={params.template}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { KokoroTTS } from 'kokoro-js';
|
||||
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { getVoices as _getVoices } from '$lib/apis/audio';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { round } from '@huggingface/transformers';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -20,6 +23,13 @@
|
||||
|
||||
let STTEngine = '';
|
||||
|
||||
let TTSEngine = '';
|
||||
let TTSEngineConfig = {};
|
||||
|
||||
let TTSModel = null;
|
||||
let TTSModelProgress = null;
|
||||
let TTSModelLoading = false;
|
||||
|
||||
let voices = [];
|
||||
let voice = '';
|
||||
|
||||
@@ -28,23 +38,37 @@
|
||||
const speedOptions = [2, 1.75, 1.5, 1.25, 1, 0.75, 0.5];
|
||||
|
||||
const getVoices = async () => {
|
||||
if ($config.audio.tts.engine === '') {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (TTSEngine === 'browser-kokoro') {
|
||||
if (!TTSModel) {
|
||||
await loadKokoro();
|
||||
}
|
||||
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(e);
|
||||
voices = Object.entries(TTSModel.voices).map(([key, value]) => {
|
||||
return {
|
||||
id: key,
|
||||
name: value.name,
|
||||
localService: false
|
||||
};
|
||||
});
|
||||
} else {
|
||||
if ($config.audio.tts.engine === '') {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
voices = res.voices;
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
const res = await _getVoices(localStorage.token).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
voices = res.voices;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -67,6 +91,9 @@
|
||||
|
||||
STTEngine = $settings?.audio?.stt?.engine ?? '';
|
||||
|
||||
TTSEngine = $settings?.audio?.tts?.engine ?? '';
|
||||
TTSEngineConfig = $settings?.audio?.tts?.engineConfig ?? {};
|
||||
|
||||
if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
|
||||
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
|
||||
} else {
|
||||
@@ -77,6 +104,51 @@
|
||||
|
||||
await getVoices();
|
||||
});
|
||||
|
||||
$: if (TTSEngine && TTSEngineConfig) {
|
||||
onTTSEngineChange();
|
||||
}
|
||||
|
||||
const onTTSEngineChange = async () => {
|
||||
if (TTSEngine === 'browser-kokoro') {
|
||||
await loadKokoro();
|
||||
}
|
||||
};
|
||||
|
||||
const loadKokoro = async () => {
|
||||
if (TTSEngine === 'browser-kokoro') {
|
||||
voices = [];
|
||||
|
||||
if (TTSEngineConfig?.dtype) {
|
||||
TTSModel = null;
|
||||
TTSModelProgress = null;
|
||||
TTSModelLoading = true;
|
||||
|
||||
const model_id = 'onnx-community/Kokoro-82M-v1.0-ONNX';
|
||||
|
||||
TTSModel = await KokoroTTS.from_pretrained(model_id, {
|
||||
dtype: TTSEngineConfig.dtype, // Options: "fp32", "fp16", "q8", "q4", "q4f16"
|
||||
device: !!navigator?.gpu ? 'webgpu' : 'wasm', // Detect WebGPU
|
||||
progress_callback: (e) => {
|
||||
TTSModelProgress = e;
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
await getVoices();
|
||||
|
||||
// const rawAudio = await tts.generate(inputText, {
|
||||
// // Use `tts.list_voices()` to list all available voices
|
||||
// voice: voice
|
||||
// });
|
||||
|
||||
// const blobUrl = URL.createObjectURL(await rawAudio.toBlob());
|
||||
// const audio = new Audio(blobUrl);
|
||||
|
||||
// audio.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
@@ -88,6 +160,8 @@
|
||||
engine: STTEngine !== '' ? STTEngine : undefined
|
||||
},
|
||||
tts: {
|
||||
engine: TTSEngine !== '' ? TTSEngine : undefined,
|
||||
engineConfig: TTSEngineConfig,
|
||||
playbackRate: playbackRate,
|
||||
voice: voice !== '' ? voice : undefined,
|
||||
defaultVoice: $config?.audio?.tts?.voice ?? '',
|
||||
@@ -107,7 +181,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 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-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={STTEngine}
|
||||
placeholder="Select an engine"
|
||||
>
|
||||
@@ -124,7 +198,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleSpeechAutoSend();
|
||||
}}
|
||||
@@ -142,11 +216,44 @@
|
||||
<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">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={TTSEngine}
|
||||
placeholder="Select an engine"
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if TTSEngine === 'browser-kokoro'}
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Kokoro.js Dtype')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={TTSEngineConfig.dtype}
|
||||
placeholder="Select dtype"
|
||||
>
|
||||
<option value="" disabled selected>Select dtype</option>
|
||||
<option value="fp32">fp32</option>
|
||||
<option value="fp16">fp16</option>
|
||||
<option value="q8">q8</option>
|
||||
<option value="q4">q4</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<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"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleResponseAutoPlayback();
|
||||
}}
|
||||
@@ -165,7 +272,7 @@
|
||||
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 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-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={playbackRate}
|
||||
>
|
||||
{#each speedOptions as option}
|
||||
@@ -176,15 +283,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
{#if $config.audio.tts.engine === ''}
|
||||
{#if TTSEngine === 'browser-kokoro'}
|
||||
{#if TTSModel}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={voice}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex gap-2 items-center">
|
||||
<Spinner className="size-4" />
|
||||
|
||||
<div class=" text-sm font-medium shimmer">
|
||||
{$i18n.t('Loading Kokoro.js...')}
|
||||
{TTSModelProgress && TTSModelProgress.status === 'progress'
|
||||
? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Please do not close the settings page while loading the model.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $config.audio.tts.engine === ''}
|
||||
<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
|
||||
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 bg-white dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={voice}
|
||||
>
|
||||
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
|
||||
@@ -215,7 +361,7 @@
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
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 bg-white dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
bind:value={voice}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if showArchiveConfirm}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { models, settings, user } from '$lib/stores';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Connection from './Connections/Connection.svelte';
|
||||
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
let config = null;
|
||||
|
||||
let showConnectionModal = false;
|
||||
|
||||
const addConnectionHandler = async (connection) => {
|
||||
config.OPENAI_API_BASE_URLS.push(connection.url);
|
||||
config.OPENAI_API_KEYS.push(connection.key);
|
||||
config.OPENAI_API_CONFIGS[config.OPENAI_API_BASE_URLS.length - 1] = connection.config;
|
||||
|
||||
await updateHandler();
|
||||
};
|
||||
|
||||
const updateHandler = async () => {
|
||||
// Remove trailing slashes
|
||||
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
|
||||
|
||||
// Check if API KEYS length is same than API URLS length
|
||||
if (config.OPENAI_API_KEYS.length !== config.OPENAI_API_BASE_URLS.length) {
|
||||
// if there are more keys than urls, remove the extra keys
|
||||
if (config.OPENAI_API_KEYS.length > config.OPENAI_API_BASE_URLS.length) {
|
||||
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.slice(
|
||||
0,
|
||||
config.OPENAI_API_BASE_URLS.length
|
||||
);
|
||||
}
|
||||
|
||||
// if there are more urls than keys, add empty keys
|
||||
if (config.OPENAI_API_KEYS.length < config.OPENAI_API_BASE_URLS.length) {
|
||||
const diff = config.OPENAI_API_BASE_URLS.length - config.OPENAI_API_KEYS.length;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
config.OPENAI_API_KEYS.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await saveSettings({
|
||||
directConnections: config
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
config = $settings?.directConnections ?? {
|
||||
OPENAI_API_BASE_URLS: [],
|
||||
OPENAI_API_KEYS: [],
|
||||
OPENAI_API_CONFIGS: {}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<AddConnectionModal direct bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if config !== null}
|
||||
<div class="">
|
||||
<div class="pr-1.5">
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div class="font-medium">{$i18n.t('Manage Direct Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
|
||||
<Connection
|
||||
bind:url
|
||||
bind:key={config.OPENAI_API_KEYS[idx]}
|
||||
bind:config={config.OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
|
||||
(key, keyIdx) => idx !== keyIdx
|
||||
);
|
||||
|
||||
let newConfig = {};
|
||||
config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
config.OPENAI_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
export let onDelete = () => {};
|
||||
export let onSubmit = () => {};
|
||||
|
||||
export let pipeline = false;
|
||||
|
||||
export let url = '';
|
||||
export let key = '';
|
||||
export let config = {};
|
||||
|
||||
let showConfigModal = false;
|
||||
</script>
|
||||
|
||||
<AddConnectionModal
|
||||
edit
|
||||
direct
|
||||
bind:show={showConfigModal}
|
||||
connection={{
|
||||
url,
|
||||
key,
|
||||
config
|
||||
}}
|
||||
{onDelete}
|
||||
onSubmit={(connection) => {
|
||||
url = connection.url;
|
||||
key = connection.key;
|
||||
config = connection.config;
|
||||
onSubmit(connection);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<Tooltip
|
||||
className="w-full relative"
|
||||
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
|
||||
url
|
||||
})}
|
||||
placement="top-start"
|
||||
>
|
||||
{#if !(config?.enable ?? true)}
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
|
||||
></div>
|
||||
{/if}
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
inputClassName=" outline-hidden bg-transparent w-full"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={key}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<Tooltip content={$i18n.t('Configure')} className="self-start">
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
on:click={() => {
|
||||
showConfigModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Cog6 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { getLanguages } from '$lib/i18n';
|
||||
import { getLanguages, changeLanguage } from '$lib/i18n';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { models, settings, theme, user } from '$lib/stores';
|
||||
@@ -46,9 +46,14 @@
|
||||
let params = {
|
||||
// Advanced
|
||||
stream_response: null,
|
||||
function_calling: null,
|
||||
seed: null,
|
||||
temperature: null,
|
||||
reasoning_effort: null,
|
||||
logit_bias: null,
|
||||
frequency_penalty: null,
|
||||
presence_penalty: null,
|
||||
repeat_penalty: null,
|
||||
repeat_last_n: null,
|
||||
mirostat: null,
|
||||
mirostat_eta: null,
|
||||
@@ -139,12 +144,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
console.log(_theme);
|
||||
};
|
||||
if (typeof window !== 'undefined' && window.applyTheme) {
|
||||
window.applyTheme();
|
||||
}
|
||||
|
||||
const themeChangeHandler = (_theme: string) => {
|
||||
theme.set(_theme);
|
||||
localStorage.setItem('theme', _theme);
|
||||
if (_theme.includes('oled')) {
|
||||
document.documentElement.style.setProperty('--color-gray-800', '#101010');
|
||||
document.documentElement.style.setProperty('--color-gray-850', '#050505');
|
||||
@@ -152,6 +155,13 @@
|
||||
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
console.log(_theme);
|
||||
};
|
||||
|
||||
const themeChangeHandler = (_theme: string) => {
|
||||
theme.set(_theme);
|
||||
localStorage.setItem('theme', _theme);
|
||||
applyTheme(_theme);
|
||||
};
|
||||
</script>
|
||||
@@ -165,7 +175,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</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"
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={selectedTheme}
|
||||
placeholder="Select a theme"
|
||||
on:change={() => themeChangeHandler(selectedTheme)}
|
||||
@@ -185,11 +195,11 @@
|
||||
<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"
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={lang}
|
||||
placeholder="Select a language"
|
||||
on:change={(e) => {
|
||||
$i18n.changeLanguage(lang);
|
||||
changeLanguage(lang);
|
||||
}}
|
||||
>
|
||||
{#each languages as language}
|
||||
@@ -216,7 +226,7 @@
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
@@ -233,13 +243,13 @@
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
<hr class="border-gray-100 dark:border-gray-850 my-3" />
|
||||
|
||||
<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"
|
||||
class="w-full rounded-lg p-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-hidden resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
@@ -258,14 +268,14 @@
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
keepAlive = keepAlive === null ? '5m' : null;
|
||||
@@ -282,7 +292,7 @@
|
||||
{#if keepAlive !== null}
|
||||
<div class="flex mt-1 space-x-2">
|
||||
<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-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||
bind:value={keepAlive}
|
||||
@@ -296,7 +306,7 @@
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleRequestFormat();
|
||||
}}
|
||||
@@ -332,11 +342,20 @@
|
||||
system: system !== '' ? system : undefined,
|
||||
params: {
|
||||
stream_response: params.stream_response !== null ? params.stream_response : undefined,
|
||||
function_calling:
|
||||
params.function_calling !== null ? params.function_calling : undefined,
|
||||
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
|
||||
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
|
||||
temperature: params.temperature !== null ? params.temperature : undefined,
|
||||
reasoning_effort:
|
||||
params.reasoning_effort !== null ? params.reasoning_effort : undefined,
|
||||
logit_bias: params.logit_bias !== null ? params.logit_bias : undefined,
|
||||
frequency_penalty:
|
||||
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
|
||||
presence_penalty:
|
||||
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
|
||||
repeat_penalty:
|
||||
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
|
||||
repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,
|
||||
mirostat: params.mirostat !== null ? params.mirostat : undefined,
|
||||
mirostat_eta: params.mirostat_eta !== null ? params.mirostat_eta : undefined,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user