Spaces:
Paused
Paused
merge upstream and reconcile
Browse files- .github/workflows/codespell.yml +25 -0
- CODE_OF_CONDUCT.md +1 -1
- backend/open_webui/config.py +3 -0
- backend/open_webui/retrieval/vector/dbs/pgvector.py +38 -2
- backend/open_webui/retrieval/web/testdata/brave.json +1 -1
- backend/open_webui/utils/auth.py +3 -3
- backend/open_webui/utils/chat.py +2 -0
- backend/requirements.txt +1 -1
- pyproject.toml +7 -0
- src/lib/components/NotificationToast.svelte +10 -3
- src/lib/components/common/Textarea.svelte +1 -1
- src/lib/components/layout/Sidebar.svelte +6 -15
- src/lib/components/layout/Sidebar/Folders.svelte +1 -1
- src/lib/stores/index.ts +3 -0
- src/routes/+layout.js +2 -2
- src/routes/+layout.svelte +45 -1
.github/workflows/codespell.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Codespell configuration is within pyproject.toml
|
| 2 |
+
---
|
| 3 |
+
name: Codespell
|
| 4 |
+
|
| 5 |
+
on:
|
| 6 |
+
push:
|
| 7 |
+
branches: [main]
|
| 8 |
+
pull_request:
|
| 9 |
+
branches: [main]
|
| 10 |
+
|
| 11 |
+
permissions:
|
| 12 |
+
contents: read
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
codespell:
|
| 16 |
+
name: Check for spelling errors
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
|
| 19 |
+
steps:
|
| 20 |
+
- name: Checkout
|
| 21 |
+
uses: actions/checkout@v4
|
| 22 |
+
- name: Annotate locations with typos
|
| 23 |
+
uses: codespell-project/codespell-problem-matcher@v1
|
| 24 |
+
- name: Codespell
|
| 25 |
+
uses: codespell-project/actions-codespell@v2
|
CODE_OF_CONDUCT.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
## Our Pledge
|
| 4 |
|
| 5 |
-
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education,
|
| 6 |
|
| 7 |
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
|
| 8 |
|
|
|
|
| 2 |
|
| 3 |
## Our Pledge
|
| 4 |
|
| 5 |
+
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
| 6 |
|
| 7 |
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
|
| 8 |
|
backend/open_webui/config.py
CHANGED
|
@@ -1211,6 +1211,9 @@ if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
|
|
| 1211 |
raise ValueError(
|
| 1212 |
"Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database."
|
| 1213 |
)
|
|
|
|
|
|
|
|
|
|
| 1214 |
|
| 1215 |
####################################
|
| 1216 |
# Information Retrieval (RAG)
|
|
|
|
| 1211 |
raise ValueError(
|
| 1212 |
"Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database."
|
| 1213 |
)
|
| 1214 |
+
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
|
| 1215 |
+
os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
|
| 1216 |
+
)
|
| 1217 |
|
| 1218 |
####################################
|
| 1219 |
# Information Retrieval (RAG)
|
backend/open_webui/retrieval/vector/dbs/pgvector.py
CHANGED
|
@@ -5,6 +5,7 @@ from sqlalchemy import (
|
|
| 5 |
create_engine,
|
| 6 |
Column,
|
| 7 |
Integer,
|
|
|
|
| 8 |
select,
|
| 9 |
text,
|
| 10 |
Text,
|
|
@@ -19,9 +20,9 @@ from pgvector.sqlalchemy import Vector
|
|
| 19 |
from sqlalchemy.ext.mutable import MutableDict
|
| 20 |
|
| 21 |
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
| 22 |
-
from open_webui.config import PGVECTOR_DB_URL
|
| 23 |
|
| 24 |
-
VECTOR_LENGTH =
|
| 25 |
Base = declarative_base()
|
| 26 |
|
| 27 |
|
|
@@ -56,6 +57,9 @@ class PgvectorClient:
|
|
| 56 |
# Ensure the pgvector extension is available
|
| 57 |
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
# Create the tables if they do not exist
|
| 60 |
# Base.metadata.create_all requires a bind (engine or connection)
|
| 61 |
# Get the connection from the session
|
|
@@ -82,6 +86,38 @@ class PgvectorClient:
|
|
| 82 |
print(f"Error during initialization: {e}")
|
| 83 |
raise
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def adjust_vector_length(self, vector: List[float]) -> List[float]:
|
| 86 |
# Adjust vector to have length VECTOR_LENGTH
|
| 87 |
current_length = len(vector)
|
|
|
|
| 5 |
create_engine,
|
| 6 |
Column,
|
| 7 |
Integer,
|
| 8 |
+
MetaData,
|
| 9 |
select,
|
| 10 |
text,
|
| 11 |
Text,
|
|
|
|
| 20 |
from sqlalchemy.ext.mutable import MutableDict
|
| 21 |
|
| 22 |
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
| 23 |
+
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
| 24 |
|
| 25 |
+
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
| 26 |
Base = declarative_base()
|
| 27 |
|
| 28 |
|
|
|
|
| 57 |
# Ensure the pgvector extension is available
|
| 58 |
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
|
| 59 |
|
| 60 |
+
# Check vector length consistency
|
| 61 |
+
self.check_vector_length()
|
| 62 |
+
|
| 63 |
# Create the tables if they do not exist
|
| 64 |
# Base.metadata.create_all requires a bind (engine or connection)
|
| 65 |
# Get the connection from the session
|
|
|
|
| 86 |
print(f"Error during initialization: {e}")
|
| 87 |
raise
|
| 88 |
|
| 89 |
+
def check_vector_length(self) -> None:
|
| 90 |
+
"""
|
| 91 |
+
Check if the VECTOR_LENGTH matches the existing vector column dimension in the database.
|
| 92 |
+
Raises an exception if there is a mismatch.
|
| 93 |
+
"""
|
| 94 |
+
metadata = MetaData()
|
| 95 |
+
metadata.reflect(bind=self.session.bind, only=["document_chunk"])
|
| 96 |
+
|
| 97 |
+
if "document_chunk" in metadata.tables:
|
| 98 |
+
document_chunk_table = metadata.tables["document_chunk"]
|
| 99 |
+
if "vector" in document_chunk_table.columns:
|
| 100 |
+
vector_column = document_chunk_table.columns["vector"]
|
| 101 |
+
vector_type = vector_column.type
|
| 102 |
+
if isinstance(vector_type, Vector):
|
| 103 |
+
db_vector_length = vector_type.dim
|
| 104 |
+
if db_vector_length != VECTOR_LENGTH:
|
| 105 |
+
raise Exception(
|
| 106 |
+
f"VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. "
|
| 107 |
+
"Cannot change vector size after initialization without migrating the data."
|
| 108 |
+
)
|
| 109 |
+
else:
|
| 110 |
+
raise Exception(
|
| 111 |
+
"The 'vector' column exists but is not of type 'Vector'."
|
| 112 |
+
)
|
| 113 |
+
else:
|
| 114 |
+
raise Exception(
|
| 115 |
+
"The 'vector' column does not exist in the 'document_chunk' table."
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
# Table does not exist yet; no action needed
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
def adjust_vector_length(self, vector: List[float]) -> List[float]:
|
| 122 |
# Adjust vector to have length VECTOR_LENGTH
|
| 123 |
current_length = len(vector)
|
backend/open_webui/retrieval/web/testdata/brave.json
CHANGED
|
@@ -683,7 +683,7 @@
|
|
| 683 |
"age": "October 29, 2022",
|
| 684 |
"extra_snippets": [
|
| 685 |
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
|
| 686 |
-
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or
|
| 687 |
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
|
| 688 |
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
|
| 689 |
]
|
|
|
|
| 683 |
"age": "October 29, 2022",
|
| 684 |
"extra_snippets": [
|
| 685 |
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
|
| 686 |
+
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or usable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
|
| 687 |
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
|
| 688 |
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
|
| 689 |
]
|
backend/open_webui/utils/auth.py
CHANGED
|
@@ -99,9 +99,9 @@ def get_current_user(
|
|
| 99 |
if request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS:
|
| 100 |
allowed_paths = [
|
| 101 |
path.strip()
|
| 102 |
-
for path in str(
|
| 103 |
-
|
| 104 |
-
)
|
| 105 |
]
|
| 106 |
|
| 107 |
if request.url.path not in allowed_paths:
|
|
|
|
| 99 |
if request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS:
|
| 100 |
allowed_paths = [
|
| 101 |
path.strip()
|
| 102 |
+
for path in str(
|
| 103 |
+
request.app.state.config.API_KEY_ALLOWED_ENDPOINTS
|
| 104 |
+
).split(",")
|
| 105 |
]
|
| 106 |
|
| 107 |
if request.url.path not in allowed_paths:
|
backend/open_webui/utils/chat.py
CHANGED
|
@@ -315,6 +315,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
|
|
| 315 |
"chat_id": data["chat_id"],
|
| 316 |
"message_id": data["id"],
|
| 317 |
"session_id": data["session_id"],
|
|
|
|
| 318 |
}
|
| 319 |
)
|
| 320 |
__event_call__ = get_event_call(
|
|
@@ -322,6 +323,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
|
|
| 322 |
"chat_id": data["chat_id"],
|
| 323 |
"message_id": data["id"],
|
| 324 |
"session_id": data["session_id"],
|
|
|
|
| 325 |
}
|
| 326 |
)
|
| 327 |
|
|
|
|
| 315 |
"chat_id": data["chat_id"],
|
| 316 |
"message_id": data["id"],
|
| 317 |
"session_id": data["session_id"],
|
| 318 |
+
"user_id": user.id,
|
| 319 |
}
|
| 320 |
)
|
| 321 |
__event_call__ = get_event_call(
|
|
|
|
| 323 |
"chat_id": data["chat_id"],
|
| 324 |
"message_id": data["id"],
|
| 325 |
"session_id": data["session_id"],
|
| 326 |
+
"user_id": user.id,
|
| 327 |
}
|
| 328 |
)
|
| 329 |
|
backend/requirements.txt
CHANGED
|
@@ -106,4 +106,4 @@ googleapis-common-protos==1.63.2
|
|
| 106 |
ldap3==2.9.1
|
| 107 |
|
| 108 |
gnupg
|
| 109 |
-
huggingface_hub
|
|
|
|
| 106 |
ldap3==2.9.1
|
| 107 |
|
| 108 |
gnupg
|
| 109 |
+
huggingface_hub
|
pyproject.toml
CHANGED
|
@@ -151,3 +151,10 @@ exclude = [
|
|
| 151 |
"chroma.sqlite3",
|
| 152 |
]
|
| 153 |
force-include = { "CHANGELOG.md" = "open_webui/CHANGELOG.md", build = "open_webui/frontend" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
"chroma.sqlite3",
|
| 152 |
]
|
| 153 |
force-include = { "CHANGELOG.md" = "open_webui/CHANGELOG.md", build = "open_webui/frontend" }
|
| 154 |
+
|
| 155 |
+
[tool.codespell]
|
| 156 |
+
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
| 157 |
+
skip = '.git*,*.svg,package-lock.json,i18n,*.lock,*.css,*-bundle.js,locales,example-doc.txt,emoji-shortcodes.json'
|
| 158 |
+
check-hidden = true
|
| 159 |
+
# ignore-regex = ''
|
| 160 |
+
ignore-words-list = 'ans'
|
src/lib/components/NotificationToast.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import { settings } from '$lib/stores';
|
| 3 |
import DOMPurify from 'dompurify';
|
| 4 |
|
| 5 |
import { marked } from 'marked';
|
|
@@ -17,8 +17,15 @@
|
|
| 17 |
}
|
| 18 |
|
| 19 |
if ($settings?.notificationSound ?? true) {
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
});
|
| 24 |
</script>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { settings, playingNotificationSound, isLastActiveTab } from '$lib/stores';
|
| 3 |
import DOMPurify from 'dompurify';
|
| 4 |
|
| 5 |
import { marked } from 'marked';
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
if ($settings?.notificationSound ?? true) {
|
| 20 |
+
if (!$playingNotificationSound && $isLastActiveTab) {
|
| 21 |
+
playingNotificationSound.set(true);
|
| 22 |
+
|
| 23 |
+
const audio = new Audio(`/audio/notification.mp3`);
|
| 24 |
+
audio.play().finally(() => {
|
| 25 |
+
// Ensure the global state is reset after the sound finishes
|
| 26 |
+
playingNotificationSound.set(false);
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
}
|
| 30 |
});
|
| 31 |
</script>
|
src/lib/components/common/Textarea.svelte
CHANGED
|
@@ -56,7 +56,7 @@
|
|
| 56 |
|
| 57 |
<style>
|
| 58 |
.placeholder::before {
|
| 59 |
-
/*
|
| 60 |
position: absolute;
|
| 61 |
content: attr(data-placeholder);
|
| 62 |
color: #adb5bd;
|
|
|
|
| 56 |
|
| 57 |
<style>
|
| 58 |
.placeholder::before {
|
| 59 |
+
/* absolute */
|
| 60 |
position: absolute;
|
| 61 |
content: attr(data-placeholder);
|
| 62 |
color: #adb5bd;
|
src/lib/components/layout/Sidebar.svelte
CHANGED
|
@@ -558,19 +558,6 @@
|
|
| 558 |
on:input={searchDebounceHandler}
|
| 559 |
placeholder={$i18n.t('Search')}
|
| 560 |
/>
|
| 561 |
-
|
| 562 |
-
<div class="absolute z-40 right-3.5 top-1">
|
| 563 |
-
<Tooltip content={$i18n.t('New folder')}>
|
| 564 |
-
<button
|
| 565 |
-
class="p-1 rounded-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 text-gray-500 dark:text-gray-500 transition"
|
| 566 |
-
on:click={() => {
|
| 567 |
-
createFolder();
|
| 568 |
-
}}
|
| 569 |
-
>
|
| 570 |
-
<Plus className=" size-3" strokeWidth="2.5" />
|
| 571 |
-
</button>
|
| 572 |
-
</Tooltip>
|
| 573 |
-
</div>
|
| 574 |
</div>
|
| 575 |
|
| 576 |
<div
|
|
@@ -605,6 +592,10 @@
|
|
| 605 |
collapsible={!search}
|
| 606 |
className="px-2 mt-0.5"
|
| 607 |
name={$i18n.t('Chats')}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
on:import={(e) => {
|
| 609 |
importChatHandler(e.detail);
|
| 610 |
}}
|
|
@@ -631,7 +622,7 @@
|
|
| 631 |
}
|
| 632 |
|
| 633 |
if (chat.pinned) {
|
| 634 |
-
const res = await toggleChatPinnedStatusById(localStorage.token, chat
|
| 635 |
}
|
| 636 |
|
| 637 |
initChatList();
|
|
@@ -661,7 +652,7 @@
|
|
| 661 |
{#if !search && $pinnedChats.length > 0}
|
| 662 |
<div class="flex flex-col space-y-1 rounded-xl">
|
| 663 |
<Folder
|
| 664 |
-
className="
|
| 665 |
bind:open={showPinnedChat}
|
| 666 |
on:change={(e) => {
|
| 667 |
localStorage.setItem('showPinnedChat', e.detail);
|
|
|
|
| 558 |
on:input={searchDebounceHandler}
|
| 559 |
placeholder={$i18n.t('Search')}
|
| 560 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
</div>
|
| 562 |
|
| 563 |
<div
|
|
|
|
| 592 |
collapsible={!search}
|
| 593 |
className="px-2 mt-0.5"
|
| 594 |
name={$i18n.t('Chats')}
|
| 595 |
+
onAdd={() => {
|
| 596 |
+
createFolder();
|
| 597 |
+
}}
|
| 598 |
+
onAddLabel={$i18n.t('New Folder')}
|
| 599 |
on:import={(e) => {
|
| 600 |
importChatHandler(e.detail);
|
| 601 |
}}
|
|
|
|
| 622 |
}
|
| 623 |
|
| 624 |
if (chat.pinned) {
|
| 625 |
+
const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
|
| 626 |
}
|
| 627 |
|
| 628 |
initChatList();
|
|
|
|
| 652 |
{#if !search && $pinnedChats.length > 0}
|
| 653 |
<div class="flex flex-col space-y-1 rounded-xl">
|
| 654 |
<Folder
|
| 655 |
+
className=""
|
| 656 |
bind:open={showPinnedChat}
|
| 657 |
on:change={(e) => {
|
| 658 |
localStorage.setItem('showPinnedChat', e.detail);
|
src/lib/components/layout/Sidebar/Folders.svelte
CHANGED
|
@@ -19,7 +19,7 @@
|
|
| 19 |
|
| 20 |
{#each folderList as folderId (folderId)}
|
| 21 |
<RecursiveFolder
|
| 22 |
-
className="
|
| 23 |
{folders}
|
| 24 |
{folderId}
|
| 25 |
on:import={(e) => {
|
|
|
|
| 19 |
|
| 20 |
{#each folderList as folderId (folderId)}
|
| 21 |
<RecursiveFolder
|
| 22 |
+
className=""
|
| 23 |
{folders}
|
| 24 |
{folderId}
|
| 25 |
on:import={(e) => {
|
src/lib/stores/index.ts
CHANGED
|
@@ -69,6 +69,9 @@ export const temporaryChatEnabled = writable(false);
|
|
| 69 |
export const scrollPaginationEnabled = writable(false);
|
| 70 |
export const currentChatPage = writable(1);
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
export type Model = OpenAIModel | OllamaModel;
|
| 73 |
|
| 74 |
type BaseModel = {
|
|
|
|
| 69 |
export const scrollPaginationEnabled = writable(false);
|
| 70 |
export const currentChatPage = writable(1);
|
| 71 |
|
| 72 |
+
export const isLastActiveTab = writable(true);
|
| 73 |
+
export const playingNotificationSound = writable(false);
|
| 74 |
+
|
| 75 |
export type Model = OpenAIModel | OllamaModel;
|
| 76 |
|
| 77 |
type BaseModel = {
|
src/routes/+layout.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
export const ssr = false;
|
| 11 |
|
| 12 |
// How to manage the trailing slashes in the URLs
|
| 13 |
-
// the URL for about page
|
| 14 |
-
// the URL for about page
|
| 15 |
// https://kit.svelte.dev/docs/page-options#trailingslash
|
| 16 |
export const trailingSlash = 'ignore';
|
|
|
|
| 10 |
export const ssr = false;
|
| 11 |
|
| 12 |
// How to manage the trailing slashes in the URLs
|
| 13 |
+
// the URL for about page will be /about with 'ignore' (default)
|
| 14 |
+
// the URL for about page will be /about/ with 'always'
|
| 15 |
// https://kit.svelte.dev/docs/page-options#trailingslash
|
| 16 |
export const trailingSlash = 'ignore';
|
src/routes/+layout.svelte
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
import {
|
| 11 |
config,
|
| 12 |
user,
|
|
|
|
| 13 |
theme,
|
| 14 |
WEBUI_NAME,
|
| 15 |
mobile,
|
|
@@ -20,7 +21,8 @@
|
|
| 20 |
chats,
|
| 21 |
currentChatPage,
|
| 22 |
tags,
|
| 23 |
-
temporaryChatEnabled
|
|
|
|
| 24 |
} from '$lib/stores';
|
| 25 |
import { goto } from '$app/navigation';
|
| 26 |
import { page } from '$app/stores';
|
|
@@ -42,7 +44,10 @@
|
|
| 42 |
|
| 43 |
setContext('i18n', i18n);
|
| 44 |
|
|
|
|
|
|
|
| 45 |
let loaded = false;
|
|
|
|
| 46 |
const BREAKPOINT = 768;
|
| 47 |
|
| 48 |
const setupSocket = async (enableWebsocket) => {
|
|
@@ -107,6 +112,15 @@
|
|
| 107 |
const { done, content, title } = data;
|
| 108 |
|
| 109 |
if (done) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
toast.custom(NotificationToast, {
|
| 111 |
componentProps: {
|
| 112 |
onClick: () => {
|
|
@@ -138,6 +152,15 @@
|
|
| 138 |
const data = event?.data?.data ?? null;
|
| 139 |
|
| 140 |
if (type === 'message') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
toast.custom(NotificationToast, {
|
| 142 |
componentProps: {
|
| 143 |
onClick: () => {
|
|
@@ -154,6 +177,27 @@
|
|
| 154 |
};
|
| 155 |
|
| 156 |
onMount(async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
theme.set(localStorage.theme);
|
| 158 |
|
| 159 |
mobile.set(window.innerWidth < BREAKPOINT);
|
|
|
|
| 10 |
import {
|
| 11 |
config,
|
| 12 |
user,
|
| 13 |
+
settings,
|
| 14 |
theme,
|
| 15 |
WEBUI_NAME,
|
| 16 |
mobile,
|
|
|
|
| 21 |
chats,
|
| 22 |
currentChatPage,
|
| 23 |
tags,
|
| 24 |
+
temporaryChatEnabled,
|
| 25 |
+
isLastActiveTab
|
| 26 |
} from '$lib/stores';
|
| 27 |
import { goto } from '$app/navigation';
|
| 28 |
import { page } from '$app/stores';
|
|
|
|
| 44 |
|
| 45 |
setContext('i18n', i18n);
|
| 46 |
|
| 47 |
+
const bc = new BroadcastChannel('active-tab-channel');
|
| 48 |
+
|
| 49 |
let loaded = false;
|
| 50 |
+
|
| 51 |
const BREAKPOINT = 768;
|
| 52 |
|
| 53 |
const setupSocket = async (enableWebsocket) => {
|
|
|
|
| 112 |
const { done, content, title } = data;
|
| 113 |
|
| 114 |
if (done) {
|
| 115 |
+
if ($isLastActiveTab) {
|
| 116 |
+
if ($settings?.notificationEnabled ?? false) {
|
| 117 |
+
new Notification(`${title} | Open WebUI`, {
|
| 118 |
+
body: content,
|
| 119 |
+
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
| 120 |
+
});
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
toast.custom(NotificationToast, {
|
| 125 |
componentProps: {
|
| 126 |
onClick: () => {
|
|
|
|
| 152 |
const data = event?.data?.data ?? null;
|
| 153 |
|
| 154 |
if (type === 'message') {
|
| 155 |
+
if ($isLastActiveTab) {
|
| 156 |
+
if ($settings?.notificationEnabled ?? false) {
|
| 157 |
+
new Notification(`${data?.user?.name} (#${event?.channel?.name}) | Open WebUI`, {
|
| 158 |
+
body: data?.content,
|
| 159 |
+
icon: data?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
toast.custom(NotificationToast, {
|
| 165 |
componentProps: {
|
| 166 |
onClick: () => {
|
|
|
|
| 177 |
};
|
| 178 |
|
| 179 |
onMount(async () => {
|
| 180 |
+
// Listen for messages on the BroadcastChannel
|
| 181 |
+
bc.onmessage = (event) => {
|
| 182 |
+
if (event.data === 'active') {
|
| 183 |
+
isLastActiveTab.set(false); // Another tab became active
|
| 184 |
+
}
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
// Set yourself as the last active tab when this tab is focused
|
| 188 |
+
const handleVisibilityChange = () => {
|
| 189 |
+
if (document.visibilityState === 'visible') {
|
| 190 |
+
isLastActiveTab.set(true); // This tab is now the active tab
|
| 191 |
+
bc.postMessage('active'); // Notify other tabs that this tab is active
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
// Add event listener for visibility state changes
|
| 196 |
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
| 197 |
+
|
| 198 |
+
// Call visibility change handler initially to set state on load
|
| 199 |
+
handleVisibilityChange();
|
| 200 |
+
|
| 201 |
theme.set(localStorage.theme);
|
| 202 |
|
| 203 |
mobile.set(window.innerWidth < BREAKPOINT);
|