+
+
- {!settings?.credentialsOnly && (
- <>
-
-
-
+
+
+
+
+
+
+
+
+
-
e.target.blur()}
- defaultValue={settings?.OllamaLLMTokenLimit}
- required={true}
- autoComplete="off"
- />
+ {loading ? (
+
+ ) : (
+ <>
+ {!basePathValue.value && (
+
+ )}
+ >
+ )}
- >
- )}
+
+
+ Enter the URL where Ollama is running.
+
+
+
);
@@ -65,8 +129,13 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
return;
}
setLoading(true);
- const { models } = await System.customModels("ollama", null, basePath);
- setCustomModels(models || []);
+ try {
+ const { models } = await System.customModels("ollama", null, basePath);
+ setCustomModels(models || []);
+ } catch (error) {
+ console.error("Failed to fetch custom models:", error);
+ setCustomModels([]);
+ }
setLoading(false);
}
findCustomModels();
@@ -75,8 +144,8 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
if (loading || customModels.length == 0) {
return (
-
);
}
return (
-
- Chat Model Selection
+
+ Ollama Model
+
+ Choose the Ollama model you want to use for your conversations.
+
);
}
diff --git a/frontend/src/hooks/useProviderEndpointAutoDiscovery.js b/frontend/src/hooks/useProviderEndpointAutoDiscovery.js
new file mode 100644
index 00000000..956b0907
--- /dev/null
+++ b/frontend/src/hooks/useProviderEndpointAutoDiscovery.js
@@ -0,0 +1,99 @@
+import { useEffect, useState } from "react";
+import System from "@/models/system";
+import showToast from "@/utils/toast";
+
+export default function useProviderEndpointAutoDiscovery({
+ provider = null,
+ initialBasePath = "",
+ ENDPOINTS = [],
+}) {
+ const [loading, setLoading] = useState(false);
+ const [basePath, setBasePath] = useState(initialBasePath);
+ const [basePathValue, setBasePathValue] = useState(initialBasePath);
+ const [autoDetectAttempted, setAutoDetectAttempted] = useState(false);
+ const [showAdvancedControls, setShowAdvancedControls] = useState(true);
+
+ async function autoDetect(isInitialAttempt = false) {
+ setLoading(true);
+ setAutoDetectAttempted(true);
+ const possibleEndpoints = [];
+ ENDPOINTS.forEach((endpoint) => {
+ possibleEndpoints.push(
+ new Promise((resolve, reject) => {
+ System.customModels(provider, null, endpoint, 2_000)
+ .then((results) => {
+ if (!results?.models || results.models.length === 0)
+ throw new Error("No models");
+ resolve({ endpoint, models: results.models });
+ })
+ .catch(() => {
+ reject(`${provider} @ ${endpoint} did not resolve.`);
+ });
+ })
+ );
+ });
+
+ const { endpoint, models } = await Promise.any(possibleEndpoints)
+ .then((resolved) => resolved)
+ .catch(() => {
+ console.error("All endpoints failed to resolve.");
+ return { endpoint: null, models: null };
+ });
+
+ if (models !== null) {
+ setBasePath(endpoint);
+ setBasePathValue(endpoint);
+ setLoading(false);
+ showToast("Provider endpoint discovered automatically.", "success", {
+ clear: true,
+ });
+ setShowAdvancedControls(false);
+ return;
+ }
+
+ setLoading(false);
+ setShowAdvancedControls(true);
+ showToast(
+ "Couldn't automatically discover the provider endpoint. Please enter it manually.",
+ "info",
+ { clear: true }
+ );
+ }
+
+ function handleAutoDetectClick(e) {
+ e.preventDefault();
+ autoDetect();
+ }
+
+ function handleBasePathChange(e) {
+ const value = e.target.value;
+ setBasePathValue(value);
+ }
+
+ function handleBasePathBlur() {
+ setBasePath(basePathValue);
+ }
+
+ useEffect(() => {
+ if (!initialBasePath && !autoDetectAttempted) autoDetect(true);
+ }, [initialBasePath, autoDetectAttempted]);
+
+ return {
+ autoDetecting: loading,
+ autoDetectAttempted,
+ showAdvancedControls,
+ setShowAdvancedControls,
+ basePath: {
+ value: basePath,
+ set: setBasePathValue,
+ onChange: handleBasePathChange,
+ onBlur: handleBasePathBlur,
+ },
+ basePathValue: {
+ value: basePathValue,
+ set: setBasePathValue,
+ },
+ handleAutoDetectClick,
+ runAutoDetect: autoDetect,
+ };
+}
diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js
index 5254e3bd..a851f4cf 100644
--- a/frontend/src/locales/en/common.js
+++ b/frontend/src/locales/en/common.js
@@ -85,6 +85,9 @@ const TRANSLATIONS = {
remove: "Remove Workspace Image",
},
delete: {
+ title: "Delete Workspace",
+ description:
+ "Delete this workspace and all of its data. This will delete the workspace for all users.",
delete: "Delete Workspace",
deleting: "Deleting Workspace...",
"confirm-start": "You are about to delete your entire",
diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js
index d0609e9a..5dc2daad 100644
--- a/frontend/src/locales/es/common.js
+++ b/frontend/src/locales/es/common.js
@@ -82,6 +82,9 @@ const TRANSLATIONS = {
remove: "Eliminar imagen del espacio de trabajo",
},
delete: {
+ title: "Eliminar Espacio de Trabajo",
+ description:
+ "Eliminar este espacio de trabajo y todos sus datos. Esto eliminará el espacio de trabajo para todos los usuarios.",
delete: "Eliminar espacio de trabajo",
deleting: "Eliminando espacio de trabajo...",
"confirm-start": "Estás a punto de eliminar tu",
diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js
index 9e7a82e8..71c37a7e 100644
--- a/frontend/src/locales/fr/common.js
+++ b/frontend/src/locales/fr/common.js
@@ -87,6 +87,9 @@ const TRANSLATIONS = {
remove: "Supprimer l'image de l'espace de travail",
},
delete: {
+ title: "Supprimer l'Espace de Travail",
+ description:
+ "Supprimer cet espace de travail et toutes ses données. Cela supprimera l'espace de travail pour tous les utilisateurs.",
delete: "Supprimer l'espace de travail",
deleting: "Suppression de l'espace de travail...",
"confirm-start": "Vous êtes sur le point de supprimer votre",
diff --git a/frontend/src/locales/ru/common.js b/frontend/src/locales/ru/common.js
index 34f9591c..da3b49f1 100644
--- a/frontend/src/locales/ru/common.js
+++ b/frontend/src/locales/ru/common.js
@@ -78,6 +78,9 @@ const TRANSLATIONS = {
remove: "Удалить изображение рабочего пространства",
},
delete: {
+ title: "Удалить Рабочее Пространство",
+ description:
+ "Удалите это рабочее пространство и все его данные. Это удалит рабочее пространство для всех пользователей.",
delete: "Удалить рабочее пространство",
deleting: "Удаление рабочего пространства...",
"confirm-start": "Вы собираетесь удалить весь ваш",
diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js
index 0f30c4f7..476533d1 100644
--- a/frontend/src/locales/zh/common.js
+++ b/frontend/src/locales/zh/common.js
@@ -84,6 +84,8 @@ const TRANSLATIONS = {
remove: "移除工作区图像",
},
delete: {
+ title: "删除工作区",
+ description: "删除此工作区及其所有数据。这将删除所有用户的工作区。",
delete: "删除工作区",
deleting: "正在删除工作区...",
"confirm-start": "您即将删除整个",
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index b922457b..095244a4 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -420,22 +420,6 @@ const System = {
return { success: false, error: e.message };
});
},
- getCanDeleteWorkspaces: async function () {
- return await fetch(`${API_BASE}/system/can-delete-workspaces`, {
- method: "GET",
- cache: "no-cache",
- headers: baseHeaders(),
- })
- .then((res) => {
- if (!res.ok) throw new Error("Could not fetch can delete workspaces.");
- return res.json();
- })
- .then((res) => res?.canDelete)
- .catch((e) => {
- console.error(e);
- return false;
- });
- },
getWelcomeMessages: async function () {
return await fetch(`${API_BASE}/system/welcome-messages`, {
method: "GET",
@@ -512,10 +496,23 @@ const System = {
return false;
});
},
- customModels: async function (provider, apiKey = null, basePath = null) {
+ customModels: async function (
+ provider,
+ apiKey = null,
+ basePath = null,
+ timeout = null
+ ) {
+ const controller = new AbortController();
+ if (!!timeout) {
+ setTimeout(() => {
+ controller.abort("Request timed out.");
+ }, timeout);
+ }
+
return fetch(`${API_BASE}/system/custom-models`, {
method: "POST",
headers: baseHeaders(),
+ signal: controller.signal,
body: JSON.stringify({
provider,
apiKey,
diff --git a/frontend/src/pages/Admin/System/index.jsx b/frontend/src/pages/Admin/System/index.jsx
index bdab765a..3924c6e8 100644
--- a/frontend/src/pages/Admin/System/index.jsx
+++ b/frontend/src/pages/Admin/System/index.jsx
@@ -8,7 +8,6 @@ import CTAButton from "@/components/lib/CTAButton";
export default function AdminSystem() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
- const [canDelete, setCanDelete] = useState(false);
const [messageLimit, setMessageLimit] = useState({
enabled: false,
limit: 10,
@@ -18,7 +17,6 @@ export default function AdminSystem() {
e.preventDefault();
setSaving(true);
await Admin.updateSystemPreferences({
- users_can_delete_workspaces: canDelete,
limit_user_messages: messageLimit.enabled,
message_limit: messageLimit.limit,
});
@@ -31,7 +29,6 @@ export default function AdminSystem() {
async function fetchSettings() {
const settings = (await Admin.systemPreferences())?.settings;
if (!settings) return;
- setCanDelete(settings?.users_can_delete_workspaces);
setMessageLimit({
enabled: settings.limit_user_messages,
limit: settings.message_limit,
@@ -71,29 +68,6 @@ export default function AdminSystem() {