thryyyyy commited on
Commit
b1b97fc
·
2 Parent(s): c8b1a80 2cf0297

merge upstream and reconcile

Browse files
.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, socio-economic 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
 
 
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 = 1536
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 useable 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
  ]
 
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(request.app.state.config.API_KEY_ALLOWED_PATHS).split(
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
- const audio = new Audio(`/audio/notification.mp3`);
21
- audio.play();
 
 
 
 
 
 
 
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
- /* abolute */
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, id);
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="px-2"
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="px-2"
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 witll be /about with 'ignore' (default)
14
- // the URL for about page witll be /about/ with 'always'
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);