Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
a0935dec
Unverified
Commit
a0935dec
authored
Mar 31, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Mar 31, 2024
Browse files
Merge branch 'dev' into feature/support_auth_by_api_key
parents
b4b56f9c
587a8c59
Changes
69
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
703 additions
and
155 deletions
+703
-155
backend/main.py
backend/main.py
+11
-5
backend/utils/webhook.py
backend/utils/webhook.py
+36
-2
kubernetes/helm/templates/ollama-statefulset.yaml
kubernetes/helm/templates/ollama-statefulset.yaml
+1
-1
kubernetes/helm/templates/webui-pvc.yaml
kubernetes/helm/templates/webui-pvc.yaml
+1
-1
kubernetes/manifest/base/webui-deployment.yaml
kubernetes/manifest/base/webui-deployment.yaml
+1
-1
kubernetes/manifest/base/webui-pvc.yaml
kubernetes/manifest/base/webui-pvc.yaml
+2
-2
package-lock.json
package-lock.json
+2
-2
package.json
package.json
+2
-3
src/app.html
src/app.html
+5
-1
src/lib/apis/litellm/index.ts
src/lib/apis/litellm/index.ts
+1
-1
src/lib/apis/openai/index.ts
src/lib/apis/openai/index.ts
+50
-0
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+54
-79
src/lib/components/chat/ModelSelector/Selector.svelte
src/lib/components/chat/ModelSelector/Selector.svelte
+389
-0
src/lib/components/chat/Settings/General.svelte
src/lib/components/chat/Settings/General.svelte
+13
-3
src/lib/components/chat/Settings/Interface.svelte
src/lib/components/chat/Settings/Interface.svelte
+44
-12
src/lib/components/chat/TagChatModal.svelte
src/lib/components/chat/TagChatModal.svelte
+20
-0
src/lib/components/common/Selector.svelte
src/lib/components/common/Selector.svelte
+37
-29
src/lib/components/common/Tags.svelte
src/lib/components/common/Tags.svelte
+2
-1
src/lib/components/common/Tags/TagInput.svelte
src/lib/components/common/Tags/TagInput.svelte
+31
-11
src/lib/components/common/Tags/TagList.svelte
src/lib/components/common/Tags/TagList.svelte
+1
-1
No files found.
backend/main.py
View file @
a0935dec
...
...
@@ -32,6 +32,7 @@ from utils.utils import get_admin_user
from
apps.rag.utils
import
rag_messages
from
config
import
(
CONFIG_DATA
,
WEBUI_NAME
,
ENV
,
VERSION
,
...
...
@@ -49,6 +50,7 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log
=
logging
.
getLogger
(
__name__
)
log
.
setLevel
(
SRC_LOG_LEVELS
[
"MAIN"
])
class
SPAStaticFiles
(
StaticFiles
):
async
def
get_response
(
self
,
path
:
str
,
scope
):
try
:
...
...
@@ -88,7 +90,6 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification
if
"docs"
in
data
:
data
=
{
**
data
}
data
[
"messages"
]
=
rag_messages
(
data
[
"docs"
],
...
...
@@ -163,11 +164,15 @@ app.mount("/rag/api/v1", rag_app)
@
app
.
get
(
"/api/config"
)
async
def
get_app_config
():
return
{
"status"
:
True
,
"name"
:
WEBUI_NAME
,
"version"
:
VERSION
,
"default_locale"
:
(
CONFIG_DATA
[
"ui"
][
"default_locale"
]
if
"ui"
in
CONFIG_DATA
and
"default_locale"
in
CONFIG_DATA
[
"ui"
]
else
"en-US"
),
"images"
:
images_app
.
state
.
ENABLED
,
"default_models"
:
webui_app
.
state
.
DEFAULT_MODELS
,
"default_prompt_suggestions"
:
webui_app
.
state
.
DEFAULT_PROMPT_SUGGESTIONS
,
...
...
@@ -191,7 +196,6 @@ class ModelFilterConfigForm(BaseModel):
async
def
update_model_filter_config
(
form_data
:
ModelFilterConfigForm
,
user
=
Depends
(
get_admin_user
)
):
app
.
state
.
MODEL_FILTER_ENABLED
=
form_data
.
enabled
app
.
state
.
MODEL_FILTER_LIST
=
form_data
.
models
...
...
@@ -201,6 +205,9 @@ async def update_model_filter_config(
openai_app
.
state
.
MODEL_FILTER_ENABLED
=
app
.
state
.
MODEL_FILTER_ENABLED
openai_app
.
state
.
MODEL_FILTER_LIST
=
app
.
state
.
MODEL_FILTER_LIST
litellm_app
.
state
.
MODEL_FILTER_ENABLED
=
app
.
state
.
MODEL_FILTER_ENABLED
litellm_app
.
state
.
MODEL_FILTER_LIST
=
app
.
state
.
MODEL_FILTER_LIST
return
{
"enabled"
:
app
.
state
.
MODEL_FILTER_ENABLED
,
"models"
:
app
.
state
.
MODEL_FILTER_LIST
,
...
...
@@ -231,7 +238,6 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
@
app
.
get
(
"/api/version"
)
async
def
get_app_config
():
return
{
"version"
:
VERSION
,
}
...
...
@@ -239,7 +245,7 @@ async def get_app_config():
@
app
.
get
(
"/api/changelog"
)
async
def
get_app_changelog
():
return
CHANGELOG
return
{
key
:
CHANGELOG
[
key
]
for
idx
,
key
in
enumerate
(
CHANGELOG
)
if
idx
<
5
}
@
app
.
get
(
"/api/version/updates"
)
...
...
backend/utils/webhook.py
View file @
a0935dec
import
json
import
requests
import
logging
from
config
import
SRC_LOG_LEVELS
,
VERSION
,
WEBUI_FAVICON_URL
,
WEBUI_NAME
log
=
logging
.
getLogger
(
__name__
)
log
.
setLevel
(
SRC_LOG_LEVELS
[
"WEBHOOK"
])
def
post_webhook
(
url
:
str
,
message
:
str
,
event_data
:
dict
)
->
bool
:
try
:
payload
=
{}
if
"https://hooks.slack.com"
in
url
:
# Slack and Google Chat Webhooks
if
"https://hooks.slack.com"
in
url
or
"https://chat.googleapis.com"
in
url
:
payload
[
"text"
]
=
message
# Discord Webhooks
elif
"https://discord.com/api/webhooks"
in
url
:
payload
[
"content"
]
=
message
# Microsoft Teams Webhooks
elif
"webhook.office.com"
in
url
:
action
=
event_data
.
get
(
"action"
,
"undefined"
)
facts
=
[
{
"name"
:
name
,
"value"
:
value
}
for
name
,
value
in
json
.
loads
(
event_data
.
get
(
"user"
,
{})).
items
()
]
payload
=
{
"@type"
:
"MessageCard"
,
"@context"
:
"http://schema.org/extensions"
,
"themeColor"
:
"0076D7"
,
"summary"
:
message
,
"sections"
:
[
{
"activityTitle"
:
message
,
"activitySubtitle"
:
f
"
{
WEBUI_NAME
}
(
{
VERSION
}
) -
{
action
}
"
,
"activityImage"
:
WEBUI_FAVICON_URL
,
"facts"
:
facts
,
"markdown"
:
True
,
}
],
}
# Default Payload
else
:
payload
=
{
**
event_data
}
log
.
debug
(
f
"payload:
{
payload
}
"
)
r
=
requests
.
post
(
url
,
json
=
payload
)
r
.
raise_for_status
()
log
.
debug
(
f
"r.text:
{
r
.
text
}
"
)
return
True
except
Exception
as
e
:
print
(
e
)
log
.
exception
(
e
)
return
False
kubernetes/helm/templates/ollama-statefulset.yaml
View file @
a0935dec
...
...
@@ -88,7 +88,7 @@ spec:
resources
:
requests
:
storage
:
{{
.Values.ollama.persistence.size | quote
}}
storageClass
:
{{
.Values.ollama.persistence.storageClass
}}
storageClass
Name
:
{{
.Values.ollama.persistence.storageClass
}}
{{
- with .Values.ollama.persistence.selector
}}
selector
:
{{
- toYaml . | nindent 8
}}
...
...
kubernetes/helm/templates/webui-pvc.yaml
View file @
a0935dec
...
...
@@ -17,7 +17,7 @@ spec:
resources
:
requests
:
storage
:
{{
.Values.webui.persistence.size
}}
storageClass
:
{{
.Values.webui.persistence.storageClass
}}
storageClass
Name
:
{{
.Values.webui.persistence.storageClass
}}
{{
- with .Values.webui.persistence.selector
}}
selector
:
{{
- toYaml . | nindent 4
}}
...
...
kubernetes/manifest/base/webui-deployment.yaml
View file @
a0935dec
...
...
@@ -35,4 +35,4 @@ spec:
volumes
:
-
name
:
webui-volume
persistentVolumeClaim
:
claimName
:
ollama-webui-pvc
\ No newline at end of file
claimName
:
open-webui-pvc
\ No newline at end of file
kubernetes/manifest/base/webui-pvc.yaml
View file @
a0935dec
...
...
@@ -2,8 +2,8 @@ apiVersion: v1
kind
:
PersistentVolumeClaim
metadata
:
labels
:
app
:
o
llama
-webui
name
:
o
llama
-webui-pvc
app
:
o
pen
-webui
name
:
o
pen
-webui-pvc
namespace
:
open-webui
spec
:
accessModes
:
[
"
ReadWriteOnce"
]
...
...
package-lock.json
View file @
a0935dec
{
"name"
:
"open-webui"
,
"version"
:
"0.1.11
4
"
,
"version"
:
"0.1.11
6
"
,
"lockfileVersion"
:
3
,
"requires"
:
true
,
"packages"
:
{
""
:
{
"name"
:
"open-webui"
,
"version"
:
"0.1.11
4
"
,
"version"
:
"0.1.11
6
"
,
"dependencies"
:
{
"@sveltejs/adapter-node"
:
"^1.3.1"
,
"async"
:
"^3.2.5"
,
...
...
package.json
View file @
a0935dec
{
"name"
:
"open-webui"
,
"version"
:
"0.1.11
5
"
,
"version"
:
"0.1.11
6
"
,
"private"
:
true
,
"scripts"
:
{
"dev"
:
"vite dev --host"
,
...
...
@@ -13,7 +13,6 @@
"lint:types"
:
"npm run check"
,
"lint:backend"
:
"pylint backend/"
,
"format"
:
"prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'"
,
"format:backend"
:
"yapf --recursive backend -p -i"
,
"i18n:parse"
:
"i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
},
"devDependencies"
:
{
...
...
src/app.html
View file @
a0935dec
...
...
@@ -9,7 +9,11 @@
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
(()
=>
{
if
(
if
(
localStorage
?.
theme
&&
localStorage
?.
theme
.
includes
(
'
oled
'
))
{
document
.
documentElement
.
style
.
setProperty
(
'
--color-gray-900
'
,
'
#000000
'
);
document
.
documentElement
.
style
.
setProperty
(
'
--color-gray-950
'
,
'
#000000
'
);
document
.
documentElement
.
classList
.
add
(
'
dark
'
);
}
else
if
(
localStorage
.
theme
===
'
light
'
||
(
!
(
'
theme
'
in
localStorage
)
&&
window
.
matchMedia
(
'
(prefers-color-scheme: light)
'
).
matches
)
)
{
...
...
src/lib/apis/litellm/index.ts
View file @
a0935dec
...
...
@@ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => {
id
:
model
.
id
,
name
:
model
.
name
??
model
.
id
,
external
:
true
,
source
:
'
l
ite
llm
'
source
:
'
L
ite
LLM
'
}))
.
sort
((
a
,
b
)
=>
{
return
a
.
name
.
localeCompare
(
b
.
name
);
...
...
src/lib/apis/openai/index.ts
View file @
a0935dec
...
...
@@ -263,3 +263,53 @@ export const synthesizeOpenAISpeech = async (
return
res
;
};
export
const
generateTitle
=
async
(
token
:
string
=
''
,
template
:
string
,
model
:
string
,
prompt
:
string
,
url
:
string
=
OPENAI_API_BASE_URL
)
=>
{
let
error
=
null
;
template
=
template
.
replace
(
/{{prompt}}/g
,
prompt
);
console
.
log
(
template
);
const
res
=
await
fetch
(
`
${
url
}
/chat/completions`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
model
:
model
,
messages
:
[
{
role
:
'
user
'
,
content
:
template
}
],
stream
:
false
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
console
.
log
(
err
);
if
(
'
detail
'
in
err
)
{
error
=
err
.
detail
;
}
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
?.
choices
[
0
]?.
message
?.
content
??
'
New Chat
'
;
};
src/lib/components/chat/ModelSelector.svelte
View file @
a0935dec
<script lang="ts">
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Select from '../common/Select.svelte';
import Selector from './ModelSelector/Selector.svelte';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
...
...
@@ -33,19 +36,19 @@
}
</script>
<div class="flex flex-col m
y-2
w-full">
<div class="flex flex-col m
t-0.5
w-full">
{#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full">
<div class="overflow-hidden w-full">
<div class="mr-
2
max-w-full">
<Select
<div class="mr-
0.5
max-w-full">
<Select
or
placeholder={$i18n.t('Select a model')}
items={$models
.filter((model) => model.name !== 'hr')
.map((model) => ({
value: model.id,
label:
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`
label:
model.name,
info: model
}))}
bind:value={selectedModel}
/>
...
...
@@ -53,10 +56,10 @@
</div>
{#if selectedModelIdx === 0}
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
<Tooltip content="Add Model">
<button
class=" self-center {selectedModelIdx === 0
? 'mr-3'
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
class=" "
{disabled}
on:click={() => {
selectedModels = [...selectedModels, ''];
...
...
@@ -73,12 +76,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
</button>
</Tooltip>
</div>
{:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content="Remove Model">
<button
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
0
? 'mr-3'
: 'mr-7'}"
{disabled}
on:click={() => {
selectedModels.splice(selectedModelIdx, 1);
...
...
@@ -96,41 +99,13 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</button>
{/if}
{#if selectedModelIdx === 0}
<button
class=" self-center dark:hover:text-gray-300"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
</div>
{/each}
</div>
<div class="text-left mt-
1
.5 ml-1 text-
xs
text-gray-500">
<div class="text-left mt-
0
.5 ml-1 text-
[0.7rem]
text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
src/lib/components/chat/ModelSelector/Selector.svelte
0 → 100644
View file @
a0935dec
<script lang="ts">
import { Select } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte';
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let value = '';
export let placeholder = 'Select a model';
export let searchEnabled = true;
export let searchPlaceholder = 'Search a model';
export let items = [{ value: 'mango', label: 'Mango' }];
let searchValue = '';
let ollamaVersion = null;
$: filteredItems = searchValue
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
: items;
const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim();
console.log($MODEL_DOWNLOAD_POOL);
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
toast.error(
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
modelTag: sanitizedModelTag
})
);
return;
}
if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) {
toast.error(
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
);
return;
}
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
toast.error(error);
return null;
});
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
try {
const { value, done } = await reader.read();
if (done) break;
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
let data = JSON.parse(line);
console.log(data);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.id) {
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
requestId: data.id,
reader,
done: false
}
});
console.log(data);
}
if (data.status) {
if (data.digest) {
let downloadProgress = 0;
if (data.completed) {
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
} else {
downloadProgress = 100;
}
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
pullProgress: downloadProgress,
digest: data.digest
}
});
} else {
toast.success(data.status);
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
done: data.status === 'success'
}
});
}
}
}
}
} catch (error) {
console.log(error);
if (typeof error !== 'string') {
error = error.message;
}
toast.error(error);
// opts.callback({ success: false, error, modelName: opts.modelName });
}
}
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
toast.success(
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
modelName: sanitizedModelTag
})
);
models.set(await getModels(localStorage.token));
} else {
toast.error('Download canceled');
}
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL
});
}
};
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
});
const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
if (reader) {
await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete $MODEL_DOWNLOAD_POOL[model];
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL
});
await deleteModel(localStorage.token, model);
toast.success(`${model} download has been canceled`);
}
};
</script>
<Select.Root
{items}
onOpenChange={async () => {
searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
selected={items.find((item) => item.value === value) ?? ''}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
}}
>
<Select.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
{placeholder}
/>
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
transition={flyAndScale}
sideOffset={4}
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
<Search className="size-4" strokeWidth="2.5" />
<input
id="model-search-input"
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none"
placeholder={searchPlaceholder}
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-72 overflow-y-auto">
{#each filteredItems as item}
<Select.Item
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-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
value={item.value}
label={item.label}
>
<div class="flex items-center gap-2">
<div class="line-clamp-1">
{item.label}
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
>{item.info?.details?.parameter_size ?? ''}</span
>
</div>
<!-- {JSON.stringify(item.info)} -->
{#if item.info.external}
<Tooltip content={item.info?.source ?? 'External'}>
<div class=" mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{:else}
<Tooltip
content={`${
item.info?.details?.quantization_level
? item.info?.details?.quantization_level + ' '
: ''
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
>
<div class=" mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
</Tooltip>
{/if}
</div>
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{:else}
<div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
</div>
</div>
{/each}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
<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-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
on:click={() => {
pullModelHandler();
}}
>
Pull "{searchValue}" from Ollama.com
</button>
{/if}
{#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"
>
<div class="flex">
<div class="-ml-2 mr-2.5 translate-y-0.5">
<svg
class="size-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
<div class="flex flex-col self-start">
<div class="line-clamp-1">
Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
: ''}
</div>
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
{$MODEL_DOWNLOAD_POOL[model].digest}
</div>
{/if}
</div>
</div>
<div class="mr-2 translate-y-0.5">
<Tooltip content="Cancel">
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
cancelModelPullHandler(model);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
{/each}
</div>
</slot>
</Select.Content>
</Select.Root>
src/lib/components/chat/Settings/General.svelte
View file @
a0935dec
...
...
@@ -14,7 +14,7 @@
export let getModels: Function;
// General
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'
, 'oled-dark'
];
let selectedTheme = 'system';
let languages = [];
...
...
@@ -91,12 +91,17 @@
});
const applyTheme = (_theme: string) => {
let themeToApply = _theme;
let themeToApply =
_theme === 'oled-dark' ? 'dark' :
_theme;
if (_theme === 'system') {
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
if (themeToApply === 'dark' && !_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#171717');
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
}
themes
.filter((e) => e !== themeToApply)
.forEach((e) => {
...
...
@@ -115,7 +120,11 @@
const themeChangeHandler = (_theme: string) => {
theme.set(_theme);
localStorage.setItem('theme', _theme);
if (_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark');
}
applyTheme(_theme);
};
</script>
...
...
@@ -136,6 +145,7 @@
>
<option value="system">⚙️ {$i18n.t('System')}</option>
<option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
...
...
src/lib/components/chat/Settings/Interface.svelte
View file @
a0935dec
<script lang="ts">
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, user } from '$lib/stores';
import { config, models,
settings,
user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
...
...
@@ -14,6 +14,7 @@
let titleAutoGenerate = true;
let responseAutoCopy = false;
let titleAutoGenerateModel = '';
let titleAutoGenerateModelExternal = '';
let fullScreenMode = false;
let titleGenerationPrompt = '';
...
...
@@ -33,7 +34,12 @@
const toggleTitleAutoGenerate = async () => {
titleAutoGenerate = !titleAutoGenerate;
saveSettings({ titleAutoGenerate: titleAutoGenerate });
saveSettings({
title: {
...$settings.title,
auto: titleAutoGenerate
}
});
};
const toggleResponseAutoCopy = async () => {
...
...
@@ -65,8 +71,13 @@
}
saveSettings({
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
title: {
...$settings.title,
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
}
});
};
...
...
@@ -77,16 +88,18 @@
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
titleAutoGenerate = settings.titleAutoGenerate ?? true;
responseAutoCopy = settings.responseAutoCopy ?? false;
showUsername = settings.showUsername ?? false;
fullScreenMode = settings.fullScreenMode ?? false;
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
titleAutoGenerate = settings?.title?.auto ?? true;
titleAutoGenerateModel = settings?.title?.model ?? '';
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
titleGenerationPrompt =
settings.title
GenerationP
rompt ??
settings
?
.title
?.p
rompt ??
$i18n.t(
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
) + ' {{prompt}}';
responseAutoCopy = settings.responseAutoCopy ?? false;
showUsername = settings.showUsername ?? false;
fullScreenMode = settings.fullScreenMode ?? false;
});
</script>
...
...
@@ -190,8 +203,9 @@
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<div class="flex w-full gap-2 pr-2">
<div class="flex-1">
<div class=" text-xs mb-1">Local Models</div>
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModel}
...
...
@@ -207,6 +221,24 @@
{/each}
</select>
</div>
<div class="flex-1">
<div class=" text-xs mb-1">External Models</div>
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModelExternal}
placeholder={$i18n.t('Select a model')}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model}
{#if model.name !== 'hr'}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
{model.name}
</option>
{/if}
{/each}
</select>
</div>
</div>
<div class="mt-3 mr-2">
...
...
src/lib/components/chat/TagChatModal.svelte
0 → 100644
View file @
a0935dec
<script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let show = false;
</script>
<Modal bind:show size="xs">
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<Tags {tags} {deleteTag} {addTag} />
</div>
</Modal>
src/lib/components/common/Select.svelte
→
src/lib/components/common/Select
or
.svelte
View file @
a0935dec
...
...
@@ -12,6 +12,9 @@
export let value = '';
export let placeholder = 'Select a model';
export let searchEnabled = true;
export let searchPlaceholder = 'Search a model';
export let items = [
{ value: 'mango', label: 'Mango' },
{ value: 'watermelon', label: 'Watermelon' },
...
...
@@ -49,22 +52,25 @@
transition={flyAndScale}
sideOffset={4}
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
<Search className="size-4" strokeWidth="2.5" />
<input
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none"
placeholder=
"S
earch
a model"
placeholder=
{s
earch
Placeholder}
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-80 overflow-y-auto">
{#each filteredItems as item}
<Select.Item
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 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-none transition-all duration-75
hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer
data-[highlighted]:bg-muted"
value={item.value}
label={item.label}
>
...
...
@@ -77,11 +83,13 @@
{/if}
</Select.Item>
{:else}
<span class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
<div>
<div class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
</span>
</div>
</div>
{/each}
</div>
</slot>
</Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root>
src/lib/components/common/Tags.svelte
View file @
a0935dec
...
...
@@ -8,7 +8,7 @@
export let addTag: Function;
</script>
<div class="flex flex-row
space-x
-0.5 line-clamp-1">
<div class="flex flex-row
flex-wrap gap
-0.5 line-clamp-1">
<TagList
{tags}
on:delete={(e) => {
...
...
@@ -17,6 +17,7 @@
/>
<TagInput
label={tags.length == 0 ? 'Add Tags' : ''}
on:add={(e) => {
addTag(e.detail);
}}
...
...
src/lib/components/common/Tags/TagInput.svelte
View file @
a0935dec
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
import { tags } from '$lib/stores';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let label = '';
let showTagInput = false;
let tagName = '';
const addTagHandler = async () => {
tagName = tagName.trim();
if (tagName !== '') {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
} else {
toast.error('Invalid Tag');
}
};
</script>
<div class="flex space-x-1 pl-1.5">
{#if showTagInput}
<div class="flex items-center">
<button
type="button"
on:click={() => {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
}}
>
<button type="button" on:click={addTagHandler}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
...
...
@@ -34,12 +41,21 @@
</button>
<input
bind:value={tagName}
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[
8
rem]"
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[
5.5
rem]"
placeholder={$i18n.t('Add a tag')}
list="tagOptions"
on:keydown={(event) => {
if (event.key === 'Enter') {
addTagHandler();
}
}}
/>
<datalist id="tagOptions">
{#each $tags as tag}
<option value={tag.name} />
{/each}
</datalist>
</div>
<!-- TODO: Tag Suggestions -->
{/if}
<button
...
...
@@ -62,4 +78,8 @@
</svg>
</div>
</button>
{#if label && !showTagInput}
<span class="text-xs pl-1.5 self-center">{label}</span>
{/if}
</div>
src/lib/components/common/Tags/TagList.svelte
View file @
a0935dec
...
...
@@ -7,7 +7,7 @@
{#each tags as tag}
<div
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-
6
00 dark:text-white"
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-
8
00 dark:text-white"
>
<div class=" text-[0.7rem] font-medium self-center line-clamp-1">
{tag.name}
...
...
Prev
1
2
3
4
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment