Commit 276b7b90 authored by Jun Siang Cheah's avatar Jun Siang Cheah
Browse files

Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search

parents b1265c9c 7b81271b
litellm_settings:
drop_params: true
model_list:
- model_name: 'HuggingFace: Mistral: Mistral 7B Instruct v0.1'
litellm_params:
model: huggingface/mistralai/Mistral-7B-Instruct-v0.1
api_key: os.environ/HF_TOKEN
max_tokens: 1024
- model_name: 'HuggingFace: Mistral: Mistral 7B Instruct v0.2'
litellm_params:
model: huggingface/mistralai/Mistral-7B-Instruct-v0.2
api_key: os.environ/HF_TOKEN
max_tokens: 1024
- model_name: 'HuggingFace: Meta: Llama 3 8B Instruct'
litellm_params:
model: huggingface/meta-llama/Meta-Llama-3-8B-Instruct
api_key: os.environ/HF_TOKEN
max_tokens: 2047
- model_name: 'HuggingFace: Mistral: Mixtral 8x7B Instruct v0.1'
litellm_params:
model: huggingface/mistralai/Mixtral-8x7B-Instruct-v0.1
api_key: os.environ/HF_TOKEN
max_tokens: 8192
- model_name: 'HuggingFace: Microsoft: Phi-3 Mini-4K-Instruct'
litellm_params:
model: huggingface/microsoft/Phi-3-mini-4k-instruct
api_key: os.environ/HF_TOKEN
max_tokens: 1024
- model_name: 'HuggingFace: Google: Gemma 7B 1.1'
litellm_params:
model: huggingface/google/gemma-1.1-7b-it
api_key: os.environ/HF_TOKEN
max_tokens: 1024
- model_name: 'HuggingFace: Yi-1.5 34B Chat'
litellm_params:
model: huggingface/01-ai/Yi-1.5-34B-Chat
api_key: os.environ/HF_TOKEN
max_tokens: 1024
- model_name: 'HuggingFace: Nous Research: Nous Hermes 2 Mixtral 8x7B DPO'
litellm_params:
model: huggingface/NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO
api_key: os.environ/HF_TOKEN
max_tokens: 2048
...@@ -34,11 +34,6 @@ fi ...@@ -34,11 +34,6 @@ fi
# Check if SPACE_ID is set, if so, configure for space # Check if SPACE_ID is set, if so, configure for space
if [ -n "$SPACE_ID" ]; then if [ -n "$SPACE_ID" ]; then
echo "Configuring for HuggingFace Space deployment" echo "Configuring for HuggingFace Space deployment"
# Copy litellm_config.yaml with specified ownership
echo "Copying litellm_config.yaml to the desired location with specified ownership..."
cp -f ./space/litellm_config.yaml ./data/litellm/config.yaml
if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then
echo "Admin user configured, creating" echo "Admin user configured, creating"
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' & WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' &
......
from apps.web.models.models import Models, ModelModel, ModelForm, ModelResponse from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
def get_model_id_from_custom_model_id(id: str): def get_model_id_from_custom_model_id(id: str):
......
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends from fastapi import HTTPException, status, Depends
from apps.web.models.users import Users from apps.webui.models.users import Users
from pydantic import BaseModel from pydantic import BaseModel
from typing import Union, Optional from typing import Union, Optional
......
...@@ -273,7 +273,6 @@ langsmith==0.1.57 ...@@ -273,7 +273,6 @@ langsmith==0.1.57
# via langchain-community # via langchain-community
# via langchain-core # via langchain-core
litellm==1.37.20 litellm==1.37.20
# via litellm
# via open-webui # via open-webui
lxml==5.2.2 lxml==5.2.2
# via unstructured # via unstructured
...@@ -396,7 +395,6 @@ pandas==2.2.2 ...@@ -396,7 +395,6 @@ pandas==2.2.2
# via open-webui # via open-webui
passlib==1.7.4 passlib==1.7.4
# via open-webui # via open-webui
# via passlib
pathspec==0.12.1 pathspec==0.12.1
# via black # via black
peewee==3.17.5 peewee==3.17.5
...@@ -454,7 +452,6 @@ pygments==2.18.0 ...@@ -454,7 +452,6 @@ pygments==2.18.0
pyjwt==2.8.0 pyjwt==2.8.0
# via litellm # via litellm
# via open-webui # via open-webui
# via pyjwt
pymysql==1.1.0 pymysql==1.1.0
# via open-webui # via open-webui
pypandoc==1.13 pypandoc==1.13
...@@ -559,6 +556,9 @@ scipy==1.13.0 ...@@ -559,6 +556,9 @@ scipy==1.13.0
# via sentence-transformers # via sentence-transformers
sentence-transformers==2.7.0 sentence-transformers==2.7.0
# via open-webui # via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4 shapely==2.0.4
# via rapidocr-onnxruntime # via rapidocr-onnxruntime
shellingham==1.5.4 shellingham==1.5.4
...@@ -659,7 +659,6 @@ uvicorn==0.22.0 ...@@ -659,7 +659,6 @@ uvicorn==0.22.0
# via fastapi # via fastapi
# via litellm # via litellm
# via open-webui # via open-webui
# via uvicorn
uvloop==0.19.0 uvloop==0.19.0
# via uvicorn # via uvicorn
validators==0.28.1 validators==0.28.1
...@@ -687,6 +686,3 @@ youtube-transcript-api==0.6.2 ...@@ -687,6 +686,3 @@ youtube-transcript-api==0.6.2
# via open-webui # via open-webui
zipp==3.18.1 zipp==3.18.1
# via importlib-metadata # via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
...@@ -273,7 +273,6 @@ langsmith==0.1.57 ...@@ -273,7 +273,6 @@ langsmith==0.1.57
# via langchain-community # via langchain-community
# via langchain-core # via langchain-core
litellm==1.37.20 litellm==1.37.20
# via litellm
# via open-webui # via open-webui
lxml==5.2.2 lxml==5.2.2
# via unstructured # via unstructured
...@@ -396,7 +395,6 @@ pandas==2.2.2 ...@@ -396,7 +395,6 @@ pandas==2.2.2
# via open-webui # via open-webui
passlib==1.7.4 passlib==1.7.4
# via open-webui # via open-webui
# via passlib
pathspec==0.12.1 pathspec==0.12.1
# via black # via black
peewee==3.17.5 peewee==3.17.5
...@@ -454,7 +452,6 @@ pygments==2.18.0 ...@@ -454,7 +452,6 @@ pygments==2.18.0
pyjwt==2.8.0 pyjwt==2.8.0
# via litellm # via litellm
# via open-webui # via open-webui
# via pyjwt
pymysql==1.1.0 pymysql==1.1.0
# via open-webui # via open-webui
pypandoc==1.13 pypandoc==1.13
...@@ -559,6 +556,9 @@ scipy==1.13.0 ...@@ -559,6 +556,9 @@ scipy==1.13.0
# via sentence-transformers # via sentence-transformers
sentence-transformers==2.7.0 sentence-transformers==2.7.0
# via open-webui # via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4 shapely==2.0.4
# via rapidocr-onnxruntime # via rapidocr-onnxruntime
shellingham==1.5.4 shellingham==1.5.4
...@@ -659,7 +659,6 @@ uvicorn==0.22.0 ...@@ -659,7 +659,6 @@ uvicorn==0.22.0
# via fastapi # via fastapi
# via litellm # via litellm
# via open-webui # via open-webui
# via uvicorn
uvloop==0.19.0 uvloop==0.19.0
# via uvicorn # via uvicorn
validators==0.28.1 validators==0.28.1
...@@ -687,6 +686,3 @@ youtube-transcript-api==0.6.2 ...@@ -687,6 +686,3 @@ youtube-transcript-api==0.6.2
# via open-webui # via open-webui
zipp==3.18.1 zipp==3.18.1
# via importlib-metadata # via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
...@@ -654,3 +654,35 @@ export const deleteAllChats = async (token: string) => { ...@@ -654,3 +654,35 @@ export const deleteAllChats = async (token: string) => {
return res; return res;
}; };
export const archiveAllChats = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archive/all`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
import { LITELLM_API_BASE_URL } from '$lib/constants';
export const getLiteLLMModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/v1/models`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
return models
? models
.map((model) => ({
id: model.id,
name: model.name ?? model.id,
external: true,
source: 'LiteLLM',
custom_info: model.custom_info
}))
.sort((a, b) => {
return a.name.localeCompare(b.name);
})
: models;
};
export const getLiteLLMModelInfo = async (token: string = '') => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/info`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
return models;
};
type AddLiteLLMModelForm = {
name: string;
model: string;
api_base: string;
api_key: string;
rpm: string;
max_tokens: string;
};
export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/new`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
model_name: payload.name,
litellm_params: {
model: payload.model,
...(payload.api_base === '' ? {} : { api_base: payload.api_base }),
...(payload.api_key === '' ? {} : { api_key: payload.api_key }),
...(isNaN(parseInt(payload.rpm)) ? {} : { rpm: parseInt(payload.rpm) }),
...(payload.max_tokens === '' ? {} : { max_tokens: payload.max_tokens })
}
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
return res;
};
export const deleteLiteLLMModel = async (token: string = '', id: string) => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/delete`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
id: id
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
return res;
};
...@@ -32,7 +32,7 @@ export const addNewModel = async (token: string, model: object) => { ...@@ -32,7 +32,7 @@ export const addNewModel = async (token: string, model: object) => {
export const getModelInfos = async (token: string = '') => { export const getModelInfos = async (token: string = '') => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/`, { const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -63,7 +63,10 @@ export const getModelInfos = async (token: string = '') => { ...@@ -63,7 +63,10 @@ export const getModelInfos = async (token: string = '') => {
export const getModelById = async (token: string, id: string) => { export const getModelById = async (token: string, id: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}`, { const searchParams = new URLSearchParams();
searchParams.append('id', id);
const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -95,7 +98,10 @@ export const getModelById = async (token: string, id: string) => { ...@@ -95,7 +98,10 @@ export const getModelById = async (token: string, id: string) => {
export const updateModelById = async (token: string, id: string, model: object) => { export const updateModelById = async (token: string, id: string, model: object) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}/update`, { const searchParams = new URLSearchParams();
searchParams.append('id', id);
const res = await fetch(`${WEBUI_API_BASE_URL}/models/update?${searchParams.toString()}`, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -128,7 +134,10 @@ export const updateModelById = async (token: string, id: string, model: object) ...@@ -128,7 +134,10 @@ export const updateModelById = async (token: string, id: string, model: object)
export const deleteModelById = async (token: string, id: string) => { export const deleteModelById = async (token: string, id: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}/delete`, { const searchParams = new URLSearchParams();
searchParams.append('id', id);
const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete?${searchParams.toString()}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
......
...@@ -164,7 +164,7 @@ export const getOllamaVersion = async (token: string = '') => { ...@@ -164,7 +164,7 @@ export const getOllamaVersion = async (token: string = '') => {
throw error; throw error;
} }
return res?.version ?? ''; return res?.version ?? false;
}; };
export const getOllamaModels = async (token: string = '') => { export const getOllamaModels = async (token: string = '') => {
......
<script lang="ts"> <script lang="ts">
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { downloadDatabase } from '$lib/apis/utils'; import { downloadDatabase } from '$lib/apis/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { config } from '$lib/stores'; import { config, user } from '$lib/stores';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getAllUserChats } from '$lib/apis/chats';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
const exportAllUserChats = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
type: 'application/json'
});
saveAs(blob, `all-chats-export-${Date.now()}.json`);
};
onMount(async () => { onMount(async () => {
// permissions = await getUserPermissions(localStorage.token); // permissions = await getUserPermissions(localStorage.token);
}); });
...@@ -23,10 +34,10 @@ ...@@ -23,10 +34,10 @@
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
{#if $config?.enable_admin_export ?? true}
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> --> <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
{#if $config?.admin_export_enabled ?? true}
<button <button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
type="button" type="button"
...@@ -55,8 +66,36 @@ ...@@ -55,8 +66,36 @@
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div> <div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button> </button>
{/if}
</div> </div>
<hr class=" dark:border-gray-700 my-1" />
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportAllUserChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Export All Chats (All Users)')}
</div>
</button>
{/if}
</div> </div>
</div> </div>
......
...@@ -39,12 +39,7 @@ ...@@ -39,12 +39,7 @@
import MessageInput from '$lib/components/chat/MessageInput.svelte'; import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte'; import Messages from '$lib/components/chat/Messages.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import { import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
LITELLM_API_BASE_URL,
OLLAMA_API_BASE_URL,
OPENAI_API_BASE_URL,
WEBUI_BASE_URL
} from '$lib/constants';
import { createOpenAITextStream } from '$lib/apis/streaming'; import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories'; import { queryMemory } from '$lib/apis/memories';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
...@@ -779,9 +774,7 @@ ...@@ -779,9 +774,7 @@
docs: docs.length > 0 ? docs : undefined, docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0 citations: docs.length > 0
}, },
model?.source?.toLowerCase() === 'litellm' `${OPENAI_API_BASE_URL}`
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
); );
// Wait until history/message have been updated // Wait until history/message have been updated
...@@ -1120,6 +1113,6 @@ ...@@ -1120,6 +1113,6 @@
{messages} {messages}
{submitPrompt} {submitPrompt}
{stopResponse} {stopResponse}
webSearchAvailable={$config.websearch ?? false} webSearchAvailable={$config.enable_websearch ?? false}
/> />
{/if} {/if}
...@@ -80,6 +80,13 @@ ...@@ -80,6 +80,13 @@
return `<code>${code.replaceAll('&amp;', '&')}</code>`; return `<code>${code.replaceAll('&amp;', '&')}</code>`;
}; };
// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
const origLinkRenderer = renderer.link;
renderer.link = (href, title, text) => {
const html = origLinkRenderer.call(renderer, href, title, text);
return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
};
const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & { const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
extensions: any; extensions: any;
...@@ -790,7 +797,7 @@ ...@@ -790,7 +797,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if $config.images && !readOnly} {#if $config.enable_image_generation && !readOnly}
<Tooltip content="Generate Image" placement="bottom"> <Tooltip content="Generate Image" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import { chats, user, config } from '$lib/stores'; import { chats, user, config } from '$lib/stores';
import { import {
archiveAllChats,
createNewChat, createNewChat,
deleteAllChats, deleteAllChats,
getAllChats, getAllChats,
...@@ -22,7 +23,10 @@ ...@@ -22,7 +23,10 @@
// Chats // Chats
let saveChatHistory = true; let saveChatHistory = true;
let importFiles; let importFiles;
let showArchiveConfirm = false;
let showDeleteConfirm = false; let showDeleteConfirm = false;
let chatImportInputElement: HTMLInputElement; let chatImportInputElement: HTMLInputElement;
$: if (importFiles) { $: if (importFiles) {
...@@ -68,14 +72,15 @@ ...@@ -68,14 +72,15 @@
saveAs(blob, `chat-export-${Date.now()}.json`); saveAs(blob, `chat-export-${Date.now()}.json`);
}; };
const exportAllUserChats = async () => { const archiveAllChatsHandler = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], { await goto('/');
type: 'application/json' await archiveAllChats(localStorage.token).catch((error) => {
toast.error(error);
}); });
saveAs(blob, `all-chats-export-${Date.now()}.json`); await chats.set(await getChatList(localStorage.token));
}; };
const deleteChats = async () => { const deleteAllChatsHandler = async () => {
await goto('/'); await goto('/');
await deleteAllChats(localStorage.token).catch((error) => { await deleteAllChats(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
...@@ -217,7 +222,8 @@ ...@@ -217,7 +222,8 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
{#if showDeleteConfirm} <div class="flex flex-col">
{#if showArchiveConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<svg <svg
...@@ -240,8 +246,8 @@ ...@@ -240,8 +246,8 @@
<button <button
class="hover:text-white transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
deleteChats(); archiveAllChatsHandler();
showDeleteConfirm = false; showArchiveConfirm = false;
}} }}
> >
<svg <svg
...@@ -260,7 +266,7 @@ ...@@ -260,7 +266,7 @@
<button <button
class="hover:text-white transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
showDeleteConfirm = false; showArchiveConfirm = false;
}} }}
> >
<svg <svg
...@@ -280,34 +286,94 @@ ...@@ -280,34 +286,94 @@
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
showDeleteConfirm = true; showArchiveConfirm = true;
}} }}
> >
<div class=" self-center mr-3"> <div class=" self-center mr-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="size-4"
> >
<path
d="M3.375 3C2.339 3 1.5 3.84 1.5 4.875v.75c0 1.036.84 1.875 1.875 1.875h17.25c1.035 0 1.875-.84 1.875-1.875v-.75C22.5 3.839 21.66 3 20.625 3H3.375Z"
/>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z" d="m3.087 9 .54 9.176A3 3 0 0 0 6.62 21h10.757a3 3 0 0 0 2.995-2.824L20.913 9H3.087Zm6.163 3.75A.75.75 0 0 1 10 12h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div> <div class=" self-center text-sm font-medium">{$i18n.t('Archive All Chats')}</div>
</button> </button>
{/if} {/if}
{#if $user?.role === 'admin' && ($config?.admin_export_enabled ?? true)} {#if showDeleteConfirm}
<hr class=" dark:border-gray-700" /> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteAllChatsHandler();
showDeleteConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showDeleteConfirm = false;
}}
>
<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>
</div>
{:else}
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
exportAllUserChats(); showDeleteConfirm = true;
}} }}
> >
<div class=" self-center mr-3"> <div class=" self-center mr-3">
...@@ -317,18 +383,16 @@ ...@@ -317,18 +383,16 @@
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z" d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
<div class=" self-center text-sm font-medium"> <div class=" self-center text-sm font-medium">{$i18n.t('Delete All Chats')}</div>
{$i18n.t('Export All Chats (All Users)')}
</div>
</button> </button>
{/if} {/if}
</div> </div>
</div>
</div> </div>
...@@ -302,7 +302,7 @@ ...@@ -302,7 +302,7 @@
system: system !== '' ? system : undefined, system: system !== '' ? system : undefined,
params: { params: {
seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined, seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined,
stop: params.stop !== null ? params.stop.split(',').filter((e) => e) : undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
temperature: params.temperature !== '' ? params.temperature : undefined, temperature: params.temperature !== '' ? params.temperature : undefined,
frequency_penalty: frequency_penalty:
params.frequency_penalty !== '' ? params.frequency_penalty : undefined, params.frequency_penalty !== '' ? params.frequency_penalty : undefined,
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -34,7 +35,8 @@ ...@@ -34,7 +35,8 @@
let updateProgress = null; let updateProgress = null;
let showExperimentalOllama = false; let showExperimentalOllama = false;
let ollamaVersion = '';
let ollamaVersion = null;
const MAX_PARALLEL_DOWNLOADS = 3; const MAX_PARALLEL_DOWNLOADS = 3;
let modelTransferring = false; let modelTransferring = false;
...@@ -56,8 +58,11 @@ ...@@ -56,8 +58,11 @@
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of $models.filter( for (const model of $models.filter(
(m) => (m) =>
m.size != null && !(m?.preset ?? false) &&
(selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx)) m.owned_by === 'ollama' &&
(selectedOllamaUrlIdx === null
? true
: (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
)) { )) {
console.log(model); console.log(model);
...@@ -446,7 +451,7 @@ ...@@ -446,7 +451,7 @@
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
{#if ollamaVersion} {#if ollamaVersion !== null}
<div class="space-y-2 pr-1.5"> <div class="space-y-2 pr-1.5">
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
...@@ -644,9 +649,12 @@ ...@@ -644,9 +649,12 @@
{#if !deleteModelTag} {#if !deleteModelTag}
<option value="" disabled selected>{$i18n.t('Select a model')}</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option >{model.name +
' (' +
(model.ollama.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
> >
{/each} {/each}
</select> </select>
...@@ -874,8 +882,14 @@ ...@@ -874,8 +882,14 @@
{/if} {/if}
</div> </div>
</div> </div>
{:else} {:else if ollamaVersion === false}
<div>Ollama Not Detected</div> <div>Ollama Not Detected</div>
{:else}
<div class="flex h-full justify-center">
<div class="my-auto">
<Spinner className="size-6" />
</div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
<script lang="ts"> <script lang="ts">
export let className: string = ''; export let className: string = 'size-5';
</script> </script>
<div class="flex justify-center text-center {className}"> <div class="flex justify-center text-center">
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" <svg class={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
><style> ><style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
......
<script lang="ts"> <script lang="ts">
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte'; import { getContext, createEventDispatcher } from 'svelte';
...@@ -13,6 +15,8 @@ ...@@ -13,6 +15,8 @@
export let show = false; export let show = false;
let searchValue = '';
let chats = []; let chats = [];
const unarchiveChatHandler = async (chatId) => { const unarchiveChatHandler = async (chatId) => {
...@@ -33,6 +37,13 @@ ...@@ -33,6 +37,13 @@
chats = await getArchivedChatList(localStorage.token); chats = await getArchivedChatList(localStorage.token);
}; };
const exportChatsHandler = async () => {
let blob = new Blob([JSON.stringify(chats)], {
type: 'application/json'
});
saveAs(blob, `archived-chat-export-${Date.now()}.json`);
};
$: if (show) { $: if (show) {
(async () => { (async () => {
chats = await getArchivedChatList(localStorage.token); chats = await getArchivedChatList(localStorage.token);
...@@ -63,10 +74,35 @@ ...@@ -63,10 +74,35 @@
</button> </button>
</div> </div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> <div class="flex flex-col w-full px-5 pb-4 dark:text-gray-200">
<div class=" flex w-full mt-2 space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={searchValue}
placeholder={$i18n.t('Search Chats')}
/>
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
{#if chats.length > 0} {#if chats.length > 0}
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll"> <div>
<div class="text-left text-sm w-full mb-3 max-h-[22rem] overflow-y-scroll">
<div class="relative overflow-x-auto"> <div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
<thead <thead
...@@ -74,12 +110,16 @@ ...@@ -74,12 +110,16 @@
> >
<tr> <tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th> <th scope="col" class="px-3 py-2 hidden md:flex">
{$i18n.t('Created At')}
</th>
<th scope="col" class="px-3 py-2 text-right" /> <th scope="col" class="px-3 py-2 text-right" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each chats as chat, idx} {#each chats.filter((c) => searchValue === '' || c.title
.toLowerCase()
.includes(searchValue.toLowerCase())) as chat, idx}
<tr <tr
class="bg-transparent {idx !== chats.length - 1 && class="bg-transparent {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs" 'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
...@@ -154,11 +194,16 @@ ...@@ -154,11 +194,16 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- {#each chats as chat}
<div>
{JSON.stringify(chat)}
</div> </div>
{/each} -->
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2 m-1">
<button
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
on:click={() => {
exportChatsHandler();
}}>Export All Archived Chats</button
>
</div>
</div> </div>
{:else} {:else}
<div class="text-left text-sm w-full mb-8"> <div class="text-left text-sm w-full mb-8">
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
let importFiles; let importFiles;
let modelsImportInputElement: HTMLInputElement; let modelsImportInputElement: HTMLInputElement;
let searchValue = '';
const deleteModelHandler = async (model) => { const deleteModelHandler = async (model) => {
console.log(model.info); console.log(model.info);
if (!model?.info) { if (!model?.info) {
...@@ -97,6 +99,49 @@ ...@@ -97,6 +99,49 @@
<div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div> <div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={searchValue}
placeholder={$i18n.t('Search Models')}
/>
</div>
<div>
<a
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
href="/workspace/models/create"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</a>
</div>
</div>
<hr class=" dark:border-gray-850 my-2.5" />
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create"> <a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create">
<div class=" self-center w-10"> <div class=" self-center w-10">
<div <div
...@@ -121,7 +166,9 @@ ...@@ -121,7 +166,9 @@
<hr class=" dark:border-gray-850" /> <hr class=" dark:border-gray-850" />
<div class=" my-2 mb-5"> <div class=" my-2 mb-5">
{#each $models as model} {#each $models.filter((m) => searchValue === '' || m.name
.toLowerCase()
.includes(searchValue.toLowerCase())) as model}
<div <div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
> >
......
...@@ -5,12 +5,7 @@ ...@@ -5,12 +5,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
LITELLM_API_BASE_URL,
OLLAMA_API_BASE_URL,
OPENAI_API_BASE_URL,
WEBUI_API_BASE_URL
} from '$lib/constants';
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores'; import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
import { cancelOllamaRequest, generateChatCompletion } from '$lib/apis/ollama'; import { cancelOllamaRequest, generateChatCompletion } from '$lib/apis/ollama';
...@@ -79,11 +74,7 @@ ...@@ -79,11 +74,7 @@
} }
] ]
}, },
model.external model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1`
? model.source === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
); );
if (res && res.ok) { if (res && res.ok) {
...@@ -150,11 +141,7 @@ ...@@ -150,11 +141,7 @@
...messages ...messages
].filter((message) => message) ].filter((message) => message)
}, },
model.external model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1`
? model.source === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
); );
let responseMessage; let responseMessage;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment