From c86fcb75b7fc9959ebe80fa44bb489b6b71fd726 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 19:59:14 -0500 Subject: [PATCH 01/71] chore --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3991050972..faa0129c65 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI) +Copyright (c) 2023- Open WebUI Inc. [Created by Timothy Jaeryang Baek] All rights reserved. Redistribution and use in source and binary forms, with or without From 85b1e21765307adf93fd3977bf1ccc2b346981b0 Mon Sep 17 00:00:00 2001 From: joaoback <156559121+joaoback@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:15:47 -0300 Subject: [PATCH 02/71] Update translation.json (pt-BR) (#19700) Translations of the new items added in the latest version. --- src/lib/i18n/locales/pt-BR/translation.json | 70 ++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index cde99b4dd3..42b24844d9 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -18,15 +18,15 @@ "{{COUNT}} words": "{{COUNT}} palavras", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} às {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "O download do {{model}} foi cancelado", - "{{NAMES}} reacted with {{REACTION}}": "", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} reagiu com {{REACTION}}", "{{user}}'s Chats": "Chats de {{user}}", "{{webUIName}} Backend Required": "Backend {{webUIName}} necessário", "*Prompt node ID(s) are required for image generation": "*Prompt node ID(s) são obrigatórios para gerar imagens", "1 Source": "1 Origem", - "A collaboration channel where people join as members": "", - "A discussion channel where access is controlled by groups and permissions": "", + "A collaboration channel where people join as members": "Um canal de colaboração onde as pessoas se juntam como membros.", + "A discussion channel where access is controlled by groups and permissions": "Um canal de discussão onde o acesso é controlado por grupos e permissões.", "A new version (v{{LATEST_VERSION}}) is now available.": "Um nova versão (v{{LATEST_VERSION}}) está disponível.", - "A private conversation between you and selected users": "", + "A private conversation between you and selected users": "Uma conversa privada entre você e usuários selecionados.", "A task model is used when performing tasks such as generating titles for chats and web search queries": "Um modelo de tarefa é usado ao realizar tarefas como gerar títulos para chats e consultas de pesquisa na web", "a user": "um usuário", "About": "Sobre", @@ -57,8 +57,8 @@ "Add Custom Prompt": "Adicionar prompt personalizado", "Add Details": "Adicionar detalhes", "Add Files": "Adicionar Arquivos", - "Add Member": "", - "Add Members": "", + "Add Member": "Adicionar membro", + "Add Members": "Adicionar membros", "Add Memory": "Adicionar Memória", "Add Model": "Adicionar Modelo", "Add Reaction": "Adicionar reação", @@ -257,7 +257,7 @@ "Citations": "Citações", "Clear memory": "Limpar memória", "Clear Memory": "Limpar Memória", - "Clear status": "", + "Clear status": "Limpar status", "click here": "Clique aqui", "Click here for filter guides.": "Clique aqui para obter instruções de filtros.", "Click here for help.": "Clique aqui para obter ajuda.", @@ -294,7 +294,7 @@ "Code Interpreter": "Intérprete de código", "Code Interpreter Engine": "Motor de interpretação de código", "Code Interpreter Prompt Template": "Modelo de Prompt do Interpretador de Código", - "Collaboration channel where people join as members": "", + "Collaboration channel where people join as members": "Canal de colaboração onde as pessoas se juntam como membros.", "Collapse": "Recolher", "Collection": "Coleção", "Color": "Cor", @@ -454,7 +454,7 @@ "Discover, download, and explore custom prompts": "Descubra, baixe e explore prompts personalizados", "Discover, download, and explore custom tools": "Descubra, baixe e explore ferramentas personalizadas", "Discover, download, and explore model presets": "Descubra, baixe e explore predefinições de modelos", - "Discussion channel where access is based on groups and permissions": "", + "Discussion channel where access is based on groups and permissions": "Canal de discussão onde o acesso é baseado em grupos e permissões.", "Display": "Exibir", "Display chat title in tab": "Exibir título do chat na aba", "Display Emoji in Call": "Exibir Emoji na Chamada", @@ -471,7 +471,7 @@ "Document": "Documento", "Document Intelligence": "Inteligência de documentos", "Document Intelligence endpoint required.": "É necessário o endpoint do Document Intelligence.", - "Document Intelligence Model": "", + "Document Intelligence Model": "Modelo de Inteligência de Documentos", "Documentation": "Documentação", "Documents": "Documentos", "does not make any external connections, and your data stays securely on your locally hosted server.": "não faz nenhuma conexão externa, e seus dados permanecem seguros no seu servidor local.", @@ -494,15 +494,15 @@ "e.g. \"json\" or a JSON schema": "por exemplo, \"json\" ou um esquema JSON", "e.g. 60": "por exemplo, 60", "e.g. A filter to remove profanity from text": "Exemplo: Um filtro para remover palavrões do texto", - "e.g. about the Roman Empire": "", + "e.g. about the Roman Empire": "Por exemplo, sobre o Império Romano.", "e.g. en": "por exemplo, en", "e.g. My Filter": "Exemplo: Meu Filtro", "e.g. My Tools": "Exemplo: Minhas Ferramentas", "e.g. my_filter": "Exemplo: my_filter", "e.g. my_tools": "Exemplo: my_tools", "e.g. pdf, docx, txt": "por exemplo, pdf, docx, txt", - "e.g. Tell me a fun fact": "", - "e.g. Tell me a fun fact about the Roman Empire": "", + "e.g. Tell me a fun fact": "Por exemplo: Conte-me uma curiosidade.", + "e.g. Tell me a fun fact about the Roman Empire": "Por exemplo: Conte-me uma curiosidade sobre o Império Romano.", "e.g. Tools for performing various operations": "Exemplo: Ferramentas para executar operações diversas", "e.g., 3, 4, 5 (leave blank for default)": "por exemplo, 3, 4, 5 (deixe em branco para o padrão)", "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "por exemplo, áudio/wav, áudio/mpeg, vídeo/* (deixe em branco para os padrões)", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "Digite a URL do servidor Docling", "Enter Document Intelligence Endpoint": "Insira o endpoint do Document Intelligence", "Enter Document Intelligence Key": "Insira a chave de inteligência do documento", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "Insira o modelo de inteligência de documentos", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "Insira os domínios separados por vírgulas (ex.: example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "Insira a chave da API Exa", "Enter External Document Loader API Key": "Insira a chave da API do carregador de documentos externo", @@ -716,8 +716,8 @@ "External Web Search URL": "URL de pesquisa na Web externa", "Fade Effect for Streaming Text": "Efeito de desbotamento para texto em streaming", "Failed to add file.": "Falha ao adicionar arquivo.", - "Failed to add members": "", - "Failed to clear status": "", + "Failed to add members": "Falha ao adicionar membros", + "Failed to clear status": "Falha ao limpar o status", "Failed to connect to {{URL}} OpenAPI tool server": "Falha ao conectar ao servidor da ferramenta OpenAPI {{URL}}", "Failed to copy link": "Falha ao copiar o link", "Failed to create API Key.": "Falha ao criar a Chave API.", @@ -731,14 +731,14 @@ "Failed to load file content.": "Falha ao carregar o conteúdo do arquivo.", "Failed to move chat": "Falha ao mover o chat", "Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência", - "Failed to remove member": "", + "Failed to remove member": "Falha ao remover membro", "Failed to render diagram": "Falha ao renderizar o diagrama", "Failed to render visualization": "Falha ao renderizar a visualização", "Failed to save connections": "Falha ao salvar conexões", "Failed to save conversation": "Falha ao salvar a conversa", "Failed to save models configuration": "Falha ao salvar a configuração dos modelos", "Failed to update settings": "Falha ao atualizar as configurações", - "Failed to update status": "", + "Failed to update status": "Falha ao atualizar o status", "Failed to upload file.": "Falha ao carregar o arquivo.", "Features": "Funcionalidades", "Features Permissions": "Permissões das Funcionalidades", @@ -832,13 +832,13 @@ "Google PSE Engine Id": "ID do Motor do Google PSE", "Gravatar": "", "Group": "Grupo", - "Group Channel": "", + "Group Channel": "Canal do grupo", "Group created successfully": "Grupo criado com sucesso", "Group deleted successfully": "Grupo excluído com sucesso", "Group Description": "Descrição do Grupo", "Group Name": "Nome do Grupo", "Group updated successfully": "Grupo atualizado com sucesso", - "groups": "", + "groups": "grupos", "Groups": "Grupos", "H1": "Título", "H2": "Subtítulo", @@ -1028,9 +1028,9 @@ "MCP": "", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "O suporte ao MCP é experimental e suas especificações mudam com frequência, o que pode levar a incompatibilidades. O suporte à especificação OpenAPI é mantido diretamente pela equipe do Open WebUI, tornando-o a opção mais confiável para compatibilidade.", "Medium": "Médio", - "Member removed successfully": "", - "Members": "", - "Members added successfully": "", + "Member removed successfully": "Membro removido com sucesso", + "Members": "Membros", + "Members added successfully": "Membros adicionados com sucesso", "Memories accessible by LLMs will be shown here.": "Memórias acessíveis por LLMs serão mostradas aqui.", "Memory": "Memória", "Memory added successfully": "Memória adicionada com sucesso", @@ -1130,7 +1130,7 @@ "No models selected": "Nenhum modelo selecionado", "No Notes": "Sem Notas", "No notes found": "Notas não encontradas", - "No pinned messages": "", + "No pinned messages": "Nenhuma mensagem fixada", "No prompts found": "Nenhum prompt encontrado", "No results": "Nenhum resultado encontrado", "No results found": "Nenhum resultado encontrado", @@ -1178,7 +1178,7 @@ "Only alphanumeric characters and hyphens are allowed in the command string.": "Apenas caracteres alfanuméricos e hífens são permitidos na string de comando.", "Only can be triggered when the chat input is in focus.": "Só pode ser acionado quando o campo de entrada do chat estiver em foco.", "Only collections can be edited, create a new knowledge base to edit/add documents.": "Somente coleções podem ser editadas. Crie uma nova base de conhecimento para editar/adicionar documentos.", - "Only invited users can access": "", + "Only invited users can access": "Somente usuários convidados podem acessar.", "Only markdown files are allowed": "Somente arquivos markdown são permitidos", "Only select users and groups with permission can access": "Somente usuários e grupos selecionados com permissão podem acessar.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ops! Parece que a URL é inválida. Por favor, verifique novamente e tente de novo.", @@ -1241,7 +1241,7 @@ "Personalization": "Personalização", "Pin": "Fixar", "Pinned": "Fixado", - "Pinned Messages": "", + "Pinned Messages": "Mensagens fixadas", "Pioneer insights": "Insights pioneiros", "Pipe": "", "Pipeline deleted successfully": "Pipeline excluído com sucesso", @@ -1284,7 +1284,7 @@ "Previous 7 days": "Últimos 7 dias", "Previous message": "Mensagem anterior", "Private": "Privado", - "Private conversation between selected users": "", + "Private conversation between selected users": "Conversa privada entre usuários selecionados", "Profile": "Perfil", "Prompt": "", "Prompt Autocompletion": "Preenchimento automático de prompts", @@ -1473,7 +1473,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Defina o número de threads de trabalho usadas para computação. Esta opção controla quantos threads são usados para processar as solicitações recebidas de forma simultânea. Aumentar esse valor pode melhorar o desempenho em cargas de trabalho de alta concorrência, mas também pode consumir mais recursos da CPU.", "Set Voice": "Definir Voz", "Set whisper model": "Definir modelo Whisper", - "Set your status": "", + "Set your status": "Defina seu status", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Define um viés fixo contra tokens que apareceram pelo menos uma vez. Um valor mais alto (por exemplo, 1,5) penalizará as repetições com mais força, enquanto um valor mais baixo (por exemplo, 0,9) será mais tolerante. Em 0, está desabilitado.", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Define um viés de escala contra tokens para penalizar repetições, com base em quantas vezes elas apareceram. Um valor mais alto (por exemplo, 1,5) penalizará as repetições com mais rigor, enquanto um valor mais baixo (por exemplo, 0,9) será mais brando. Em 0, está desabilitado.", "Sets how far back for the model to look back to prevent repetition.": "Define até que ponto o modelo deve olhar para trás para evitar repetições.", @@ -1526,9 +1526,9 @@ "Start a new conversation": "Iniciar uma nova conversa", "Start of the channel": "Início do canal", "Start Tag": "Tag inicial", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "Status", + "Status cleared successfully": "Status liberado com sucesso", + "Status updated successfully": "Status atualizado com sucesso", "Status Updates": "Atualizações de status", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "Passos", @@ -1544,7 +1544,7 @@ "STT Model": "Modelo STT", "STT Settings": "Configurações STT", "Stylized PDF Export": "Exportação de PDF estilizado", - "Subtitle": "", + "Subtitle": "Legenda", "Success": "Sucesso", "Successfully imported {{userCount}} users.": "{{userCount}} usuários importados com sucesso.", "Successfully updated.": "Atualizado com sucesso.", @@ -1695,7 +1695,7 @@ "Update and Copy Link": "Atualizar e Copiar Link", "Update for the latest features and improvements.": "Atualizar para as novas funcionalidades e melhorias.", "Update password": "Atualizar senha", - "Update your status": "", + "Update your status": "Atualize seu status", "Updated": "Atualizado", "Updated at": "Atualizado em", "Updated At": "Atualizado Em", @@ -1749,7 +1749,7 @@ "View Replies": "Ver respostas", "View Result from **{{NAME}}**": "Ver resultado de **{{NAME}}**", "Visibility": "Visibilidade", - "Visible to all users": "", + "Visible to all users": "Visível para todos os usuários", "Vision": "Visão", "Voice": "Voz", "Voice Input": "Entrada de voz", @@ -1777,7 +1777,7 @@ "What are you trying to achieve?": "O que está tentando alcançar?", "What are you working on?": "No que está trabalhando?", "What's New in": "O que há de novo em", - "What's on your mind?": "", + "What's on your mind?": "O que você têm em mente?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Quando habilitado, o modelo responderá a cada mensagem de chat em tempo real, gerando uma resposta assim que o usuário enviar uma mensagem. Este modo é útil para aplicativos de chat ao vivo, mas pode impactar o desempenho em hardware mais lento.", "wherever you are": "onde quer que você esteja.", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Se a saída deve ser paginada. Cada página será separada por uma régua horizontal e um número de página. O padrão é Falso.", From 964333ed65bae29a0b403ee5568fc2ecc4d62b00 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Thu, 4 Dec 2025 06:16:03 +0800 Subject: [PATCH 03/71] i18n: improve Chinese translation (#19703) --- src/lib/i18n/locales/zh-CN/translation.json | 22 ++++++++++----------- src/lib/i18n/locales/zh-TW/translation.json | 22 ++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index b4c2e4b300..c8d7b350e8 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -257,7 +257,7 @@ "Citations": "引用", "Clear memory": "清除记忆", "Clear Memory": "清除记忆", - "Clear status": "", + "Clear status": "清除状态", "click here": "点击此处", "Click here for filter guides.": "点击此处查看筛选指南", "Click here for help.": "点击此处获取帮助", @@ -471,7 +471,7 @@ "Document": "文档", "Document Intelligence": "Document Intelligence", "Document Intelligence endpoint required.": "Document Intelligence 接口地址是必填项。", - "Document Intelligence Model": "", + "Document Intelligence Model": "Document Intelligence 模型", "Documentation": "帮助文档", "Documents": "文档", "does not make any external connections, and your data stays securely on your locally hosted server.": "不会与外部建立任何连接,您的数据会安全地存储在本地托管的服务器上。", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "输入 Docling 服务器接口地址", "Enter Document Intelligence Endpoint": "输入 Document Intelligence 端点", "Enter Document Intelligence Key": "输入 Document Intelligence 密钥", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "输入 Document Intelligence 模型", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "输入域名,多个域名用逗号分隔(例如:example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "输入 Exa API 密钥", "Enter External Document Loader API Key": "输入外部文档加载器接口密钥", @@ -717,7 +717,7 @@ "Fade Effect for Streaming Text": "流式输出内容时启用动态渐显效果", "Failed to add file.": "添加文件失败", "Failed to add members": "添加成员失败", - "Failed to clear status": "", + "Failed to clear status": "清除状态失败", "Failed to connect to {{URL}} OpenAPI tool server": "连接到 {{URL}} OpenAPI 工具服务器失败", "Failed to copy link": "复制链接失败", "Failed to create API Key.": "创建接口密钥失败", @@ -738,7 +738,7 @@ "Failed to save conversation": "保存对话失败", "Failed to save models configuration": "保存模型配置失败", "Failed to update settings": "更新设置失败", - "Failed to update status": "", + "Failed to update status": "更新状态失败", "Failed to upload file.": "上传文件失败", "Features": "功能", "Features Permissions": "功能权限", @@ -1471,7 +1471,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "设置用于计算的工作线程数量。该选项可控制并发处理传入请求的线程数量。增加该值可以提高高并发工作负载下的性能,但也可能消耗更多的 CPU 资源。", "Set Voice": "设置音色", "Set whisper model": "设置 whisper 模型", - "Set your status": "", + "Set your status": "设置您的状态", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "对至少出现过一次的标记设置固定偏置值。较高的值(例如 1.5)表示严厉惩罚重复,而较低的值(例如 0.9)则表示相对宽松。当值为 0 时,此功能将被禁用。", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "根据标记出现的次数,设置缩放偏置值惩罚重复。较高的值(例如 1.5)表示严厉惩罚重复,而较低的值(例如 0.9)则表示相对宽松。当值为 0 时,此功能将被禁用。", "Sets how far back for the model to look back to prevent repetition.": "设置模型回溯的范围,以防止重复。", @@ -1524,9 +1524,9 @@ "Start a new conversation": "开始新对话", "Start of the channel": "频道起点", "Start Tag": "起始标签", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "状态", + "Status cleared successfully": "状态已清除", + "Status updated successfully": "状态已更新", "Status Updates": "显示实时回答状态", "STDOUT/STDERR": "标准输出/标准错误", "Steps": "迭代步数", @@ -1693,7 +1693,7 @@ "Update and Copy Link": "更新和复制链接", "Update for the latest features and improvements.": "更新以获取最新功能与优化", "Update password": "更新密码", - "Update your status": "", + "Update your status": "更新您的状态", "Updated": "已更新", "Updated at": "更新于", "Updated At": "更新于", @@ -1775,7 +1775,7 @@ "What are you trying to achieve?": "您想要达到什么目标?", "What are you working on?": "您在忙于什么?", "What's New in": "最近更新内容于", - "What's on your mind?": "", + "What's on your mind?": "聊聊您的想法吧!", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "启用后,模型将实时回答每条对话信息,在用户发送信息后立即生成回答。这种模式适用于即时对话应用,但在性能较低的设备上可能会影响响应速度", "wherever you are": "纵使身在远方,亦与世界同频", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "是否对输出内容进行分页。每页之间将用水平分隔线和页码隔开。默认为关闭", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index e0f3c62aa7..6e610e3fa1 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -257,7 +257,7 @@ "Citations": "引用", "Clear memory": "清除記憶", "Clear Memory": "清除記憶", - "Clear status": "", + "Clear status": "清除狀態", "click here": "點選此處", "Click here for filter guides.": "點選此處檢視篩選器指南。", "Click here for help.": "點選此處取得協助。", @@ -471,7 +471,7 @@ "Document": "檔案", "Document Intelligence": "Document Intelligence", "Document Intelligence endpoint required.": "需要提供 Document Intelligence 端點。", - "Document Intelligence Model": "", + "Document Intelligence Model": "Document Intelligence 模型", "Documentation": "說明檔案", "Documents": "檔案", "does not make any external connections, and your data stays securely on your locally hosted server.": "不會建立任何外部連線,而且您的資料會安全地儲存在您本機伺服器上。", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "請輸入 Docling 伺服器 URL", "Enter Document Intelligence Endpoint": "輸入 Document Intelligence 端點", "Enter Document Intelligence Key": "輸入 Document Intelligence 金鑰", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "輸入 Document Intelligence 模型", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "輸入網域,以逗號分隔(例如:example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "輸入 Exa API 金鑰", "Enter External Document Loader API Key": "請輸入外部文件載入器 API 金鑰", @@ -717,7 +717,7 @@ "Fade Effect for Streaming Text": "串流文字淡入效果", "Failed to add file.": "新增檔案失敗。", "Failed to add members": "新增成員失敗", - "Failed to clear status": "", + "Failed to clear status": "清除狀態失敗", "Failed to connect to {{URL}} OpenAPI tool server": "無法連線至 {{URL}} OpenAPI 工具伺服器", "Failed to copy link": "複製連結失敗", "Failed to create API Key.": "建立 API 金鑰失敗。", @@ -738,7 +738,7 @@ "Failed to save conversation": "儲存對話失敗", "Failed to save models configuration": "儲存模型設定失敗", "Failed to update settings": "更新設定失敗", - "Failed to update status": "", + "Failed to update status": "更新狀態失敗", "Failed to upload file.": "上傳檔案失敗。", "Features": "功能", "Features Permissions": "功能權限", @@ -1471,7 +1471,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "設定用於計算的工作執行緒數量。此選項控制使用多少執行緒來同時處理傳入的請求。增加此值可以在高併發工作負載下提升效能,但也可能消耗更多 CPU 資源。", "Set Voice": "設定語音", "Set whisper model": "設定 whisper 模型", - "Set your status": "", + "Set your status": "設定您的狀態", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "對至少出現過一次的 token 設定統一的偏差值。較高的值(例如:1.5)會更強烈地懲罰重複,而較低的值(例如:0.9)會更寬容。設為 0 時,此功能將停用。", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "根據 token 出現的次數,設定一個縮放偏差值來懲罰重複。較高的值(例如:1.5)會更強烈地懲罰重複,而較低的值(例如:0.9)會更寬容。設為 0 時,此功能將停用。", "Sets how far back for the model to look back to prevent repetition.": "設定模型要回溯多遠來防止重複。", @@ -1524,9 +1524,9 @@ "Start a new conversation": "開始新對話", "Start of the channel": "頻道起點", "Start Tag": "起始標籤", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "狀態", + "Status cleared successfully": "狀態已清除", + "Status updated successfully": "狀態已更新", "Status Updates": "顯示實時回答狀態", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "迭代步數", @@ -1693,7 +1693,7 @@ "Update and Copy Link": "更新並複製連結", "Update for the latest features and improvements.": "更新以獲得最新功能和改進。", "Update password": "更新密碼", - "Update your status": "", + "Update your status": "更新您的狀態", "Updated": "已更新", "Updated at": "更新於", "Updated At": "更新於", @@ -1775,7 +1775,7 @@ "What are you trying to achieve?": "您正在試圖完成什麼?", "What are you working on?": "您現在的工作是什麼?", "What's New in": "新功能", - "What's on your mind?": "", + "What's on your mind?": "聊聊您的想法?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "啟用時,模型將即時回應每個對話訊息,在使用者傳送訊息後立即生成回應。此模式適用於即時對話應用程式,但在較慢的硬體上可能會影響效能。", "wherever you are": "無論您在何處", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "是否啟用輸出分頁功能,每頁會以水平線與頁碼分隔。預設為 False。", From b5607757131031c1a85dccebacf3c8c400ce6a9c Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Thu, 4 Dec 2025 06:16:38 +0800 Subject: [PATCH 04/71] fix: do not display the move button when no folders (#19705) --- src/lib/components/layout/Navbar/Menu.svelte | 54 +++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index a64e438ef0..6d5d478719 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -437,33 +437,37 @@ {#if !$temporaryChatEnabled && chat?.id}
- - - + {#if $folders.length > 0} + + + -
{$i18n.t('Move')}
-
- - {#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder} - { - moveChatHandler(chat?.id, folder?.id); - }} - > - +
{$i18n.t('Move')}
+
+ + {#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder} + {#if folder?.id} + { + moveChatHandler(chat.id, folder.id); + }} + > + -
{folder?.name ?? 'Folder'}
-
- {/each} -
-
+
{folder.name ?? 'Folder'}
+ + {/if} + {/each} + + + {/if} Date: Wed, 3 Dec 2025 17:17:05 -0500 Subject: [PATCH 05/71] fix: UI scale buttons (- and +) (#19699) --- src/lib/components/chat/Settings/Interface.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index e1bf8aa64b..b905e09b11 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -364,7 +364,7 @@ type="button" class="rounded-lg p-1 transition outline-gray-200 hover:bg-gray-100 dark:outline-gray-700 dark:hover:bg-gray-800" on:click={() => { - textScale = Math.max(1, textScale); + textScale = Math.max(1, parseFloat((textScale - 0.1).toFixed(2))); setTextScaleHandler(textScale); }} aria-labelledby="ui-scale-label" @@ -397,7 +397,7 @@ type="button" class="rounded-lg p-1 transition outline-gray-200 hover:bg-gray-100 dark:outline-gray-700 dark:hover:bg-gray-800" on:click={() => { - textScale = Math.min(1.5, textScale); + textScale = Math.min(1.5, parseFloat((textScale + 0.1).toFixed(2))); setTextScaleHandler(textScale); }} aria-labelledby="ui-scale-label" From 54b7ec56d6bcd2d79addc1694b757dab18cf18c5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 17:52:44 -0500 Subject: [PATCH 06/71] enh/refac: channels message lazy load data --- backend/open_webui/routers/channels.py | 59 +++++++++++++++++-- src/lib/apis/channels/index.ts | 38 ++++++++++++ src/lib/components/channel/Messages.svelte | 1 + .../channel/Messages/Message.svelte | 25 +++++++- 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 0dff67da3e..a6d9323d32 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -5,7 +5,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks from pydantic import BaseModel - +from pydantic import field_validator from open_webui.socket.main import ( emit_to_users, @@ -666,7 +666,16 @@ async def delete_channel_by_id( class MessageUserResponse(MessageResponse): - pass + data: bool | None = None + + @field_validator("data", mode="before") + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) @router.get("/{id}/messages", response_model=list[MessageUserResponse]) @@ -1108,7 +1117,7 @@ async def post_new_message( ############################ -@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageUserResponse]) +@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageResponse]) async def get_channel_message( id: str, message_id: str, user=Depends(get_verified_user) ): @@ -1142,7 +1151,7 @@ async def get_channel_message( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() ) - return MessageUserResponse( + return MessageResponse( **{ **message.model_dump(), "user": UserNameResponse( @@ -1152,6 +1161,48 @@ async def get_channel_message( ) +############################ +# GetChannelMessageData +############################ + + +@router.get("/{id}/messages/{message_id}/data", response_model=Optional[dict]) +async def get_channel_message_data( + id: str, message_id: str, user=Depends(get_verified_user) +): + channel = Channels.get_channel_by_id(id) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if channel.type in ["group", "dm"]: + if not Channels.is_user_channel_member(channel.id, user.id): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + else: + if user.role != "admin" and not has_access( + user.id, type="read", access_control=channel.access_control + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + + message = Messages.get_message_by_id(message_id) + if not message: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if message.channel_id != id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + return message.data + + ############################ # PinChannelMessage ############################ diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index 0731b2ea9f..44817e97ef 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -491,6 +491,44 @@ export const getChannelThreadMessages = async ( return res; }; +export const getMessageData = async ( + token: string = '', + channel_id: string, + message_id: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/data`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 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.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + type MessageForm = { temp_id?: string; reply_to_id?: string; diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte index b0d1fb54db..7ad8798297 100644 --- a/src/lib/components/channel/Messages.svelte +++ b/src/lib/components/channel/Messages.svelte @@ -126,6 +126,7 @@ {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)} { + if (message && message?.data) { + const res = await getMessageData(localStorage.token, channel?.id, message.id); + if (res) { + message.data = res; + } + } + }; + + onMount(async () => { + if (message && message?.data) { + await loadMessageData(); + } + }); {/if} - {#if (message?.data?.files ?? []).length > 0} + {#if message?.data === true} + +
+ +
+ {:else if (message?.data?.files ?? []).length > 0}
{#each message?.data?.files as file}
From 39f778c275e13f6ebef9b64c6893393f3af71f3a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 18:22:00 -0500 Subject: [PATCH 07/71] refac --- backend/open_webui/models/messages.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 98be21463d..5b068b6449 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -9,7 +9,7 @@ from open_webui.models.users import Users, User, UserNameResponse from open_webui.models.channels import Channels, ChannelMember -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON from sqlalchemy import or_, func, select, and_, text from sqlalchemy.sql import exists @@ -108,11 +108,24 @@ class MessageUserResponse(MessageModel): user: Optional[UserNameResponse] = None +class MessageUserSlimResponse(MessageUserResponse): + data: bool | None = None + + @field_validator("data", mode="before") + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) + + class MessageReplyToResponse(MessageUserResponse): - reply_to_message: Optional[MessageUserResponse] = None + reply_to_message: Optional[MessageUserSlimResponse] = None -class MessageWithReactionsResponse(MessageUserResponse): +class MessageWithReactionsResponse(MessageUserSlimResponse): reactions: list[Reactions] From 22f1b764a7ea1add0a896906a9ef00b4b6743adc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 19:06:02 -0500 Subject: [PATCH 08/71] refac/perf: channel image upload behaviour --- backend/open_webui/routers/files.py | 2 +- src/lib/apis/files/index.ts | 14 +++++++++++-- .../components/channel/MessageInput.svelte | 21 ++++++++----------- .../channel/Messages/Message.svelte | 8 +++++-- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 8af921bc7a..e10722c0c8 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -179,7 +179,7 @@ def upload_file_handler( user=Depends(get_verified_user), background_tasks: Optional[BackgroundTasks] = None, ): - log.info(f"file.content_type: {file.content_type}") + log.info(f"file.content_type: {file.content_type} {process}") if isinstance(metadata, str): try: diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts index 8351393e3c..07042c4ade 100644 --- a/src/lib/apis/files/index.ts +++ b/src/lib/apis/files/index.ts @@ -1,16 +1,26 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; import { splitStream } from '$lib/utils'; -export const uploadFile = async (token: string, file: File, metadata?: object | null) => { +export const uploadFile = async ( + token: string, + file: File, + metadata?: object | null, + process?: boolean | null +) => { const data = new FormData(); data.append('file', file); if (metadata) { data.append('metadata', JSON.stringify(metadata)); } + const searchParams = new URLSearchParams(); + if (process !== undefined && process !== null) { + searchParams.append('process', String(process)); + } + let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/files/?${searchParams.toString()}`, { method: 'POST', headers: { Accept: 'application/json', diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index fef7c3a078..337a3affb8 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -421,13 +421,10 @@ imageUrl = await compressImageHandler(imageUrl, $settings, $config); } - files = [ - ...files, - { - type: 'image', - url: `${imageUrl}` - } - ]; + const blob = await (await fetch(imageUrl)).blob(); + const compressedFile = new File([blob], file.name, { type: file.type }); + + uploadFileHandler(compressedFile, false); }; reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file); @@ -437,7 +434,7 @@ }); }; - const uploadFileHandler = async (file) => { + const uploadFileHandler = async (file, process = true) => { const tempItemId = uuidv4(); const fileItem = { type: 'file', @@ -461,7 +458,6 @@ try { // During the file upload, file content is automatically extracted. - // If the file is an audio file, provide the language for STT. let metadata = null; if ( @@ -473,7 +469,7 @@ }; } - const uploadedFile = await uploadFile(localStorage.token, file, metadata); + const uploadedFile = await uploadFile(localStorage.token, file, metadata, process); if (uploadedFile) { console.info('File upload completed:', { @@ -492,6 +488,7 @@ fileItem.id = uploadedFile.id; fileItem.collection_name = uploadedFile?.meta?.collection_name || uploadedFile?.collection_name; + fileItem.content_type = uploadedFile.meta?.content_type || uploadedFile.content_type; fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`; files = files; @@ -807,11 +804,11 @@ {#if files.length > 0}
{#each files as file, fileIdx} - {#if file.type === 'image'} + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index c54a96d528..a31f7b0412 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -341,8 +341,12 @@
{#each message?.data?.files as file}
- {#if file.type === 'image'} - {file.name} + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')} + {file.name} {:else} Date: Wed, 3 Dec 2025 19:15:30 -0500 Subject: [PATCH 09/71] enh: channels video file upload behaviour --- src/lib/components/channel/Messages/Message.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index a31f7b0412..ab52a84eb6 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -347,6 +347,12 @@ alt={file.name} imageClassName=" max-h-96 rounded-lg" /> + {:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')} + {:else} Date: Wed, 3 Dec 2025 19:31:23 -0500 Subject: [PATCH 10/71] refac --- backend/open_webui/routers/channels.py | 6 ++++++ backend/open_webui/utils/files.py | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index a6d9323d32..58cdcdc661 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -39,6 +39,8 @@ from open_webui.models.messages import ( ) +from open_webui.utils.files import get_image_base64_from_file_id + from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS @@ -915,6 +917,10 @@ async def model_response_handler(request, channel, message, user): for file in thread_message_files: if file.get("type", "") == "image": images.append(file.get("url", "")) + elif file.get("content_type", "").startswith("image/"): + image = get_image_base64_from_file_id(file.get("id", "")) + if image: + images.append(image) thread_history_string = "\n\n".join(thread_history) system_message = { diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py index 4f9564b7d4..cd94a41144 100644 --- a/backend/open_webui/utils/files.py +++ b/backend/open_webui/utils/files.py @@ -10,7 +10,11 @@ from fastapi import ( Request, UploadFile, ) +from typing import Optional +from pathlib import Path +from open_webui.storage.provider import Storage +from open_webui.models.files import Files from open_webui.routers.files import upload_file_handler import mimetypes @@ -113,3 +117,26 @@ def get_file_url_from_base64(request, base64_file_string, metadata, user): elif "data:audio/wav;base64" in base64_file_string: return get_audio_url_from_base64(request, base64_file_string, metadata, user) return None + + +def get_image_base64_from_file_id(id: str) -> Optional[str]: + file = Files.get_file_by_id(id) + if not file: + return None + + try: + file_path = Storage.get_file(file.path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + import base64 + + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + content_type, _ = mimetypes.guess_type(file_path.name) + return f"data:{content_type};base64,{encoded_string}" + else: + return None + except Exception as e: + return None From 5c2df97f04cce5cb7087d288f816f91a739688c1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 4 Dec 2025 14:43:23 -0500 Subject: [PATCH 11/71] fix: user channels issue --- src/lib/components/layout/Sidebar.svelte | 18 +++++++---- src/routes/+layout.svelte | 39 ++++++++++++++++-------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 272e6f1c44..e292b4b3d3 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -182,12 +182,18 @@ const initChannels = async () => { // default (none), group, dm type - await channels.set( - (await getChannels(localStorage.token)).sort( - (a, b) => - ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) - ) - ); + const res = await getChannels(localStorage.token).catch((error) => { + return null; + }); + + if (res) { + await channels.set( + res.sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) + ) + ); + } }; const initChatList = async () => { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c4a8b6c5b2..288a0668a2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -487,12 +487,19 @@ // handle channel created event if (event.data?.type === 'channel:created') { - await channels.set( - (await getChannels(localStorage.token)).sort( - (a, b) => - ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) - ) - ); + const res = await getChannels(localStorage.token).catch(async (error) => { + return null; + }); + + if (res) { + await channels.set( + res.sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) + ) + ); + } + return; } @@ -531,13 +538,19 @@ }) ); } else { - await channels.set( - (await getChannels(localStorage.token)).sort( - (a, b) => - ['', null, 'group', 'dm'].indexOf(a.type) - - ['', null, 'group', 'dm'].indexOf(b.type) - ) - ); + const res = await getChannels(localStorage.token).catch(async (error) => { + return null; + }); + + if (res) { + await channels.set( + res.sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - + ['', null, 'group', 'dm'].indexOf(b.type) + ) + ); + } } } From d27e019af311f8deda0e5aa6f039f28a00f8c5be Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:58:04 +0100 Subject: [PATCH 12/71] chore: update qdrant dependency to fix Startup Warning (#19757) * Update pyproject.toml * Update requirements.txt --- backend/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index a1a8034959..ef2007c430 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -116,7 +116,7 @@ PyMySQL==1.1.1 boto3==1.41.5 pymilvus==2.6.4 -qdrant-client==1.14.3 +qdrant-client==1.16.1 playwright==1.56.0 # Caution: version must match docker-compose.playwright.yaml elasticsearch==9.1.0 pinecone==6.0.2 diff --git a/pyproject.toml b/pyproject.toml index 10dd3259e4..ffa7d8d453 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,7 @@ all = [ "playwright==1.56.0", "elasticsearch==9.1.0", - "qdrant-client==1.14.3", + "qdrant-client==1.16.1", "weaviate-client==4.17.0", "pymilvus==2.6.4", "pinecone==6.0.2", From 1779090bdbf4d403e4dbf5df47f11eb86874dd57 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:59:09 +0100 Subject: [PATCH 13/71] fix: add missing env var parameter pass through for enable async embedding (#19748) * Add enable_async parameter to embedding function * Add enable_async parameter to RAG configuration --- backend/open_webui/main.py | 1 + backend/open_webui/routers/retrieval.py | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 21a1aee043..dc4468e8e7 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1031,6 +1031,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( if app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" else None ), + enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING, ) app.state.RERANKING_FUNCTION = get_reranking_function( diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index b7ed993895..cc2457eba7 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1401,6 +1401,7 @@ def save_docs_to_vector_db( if request.app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" else None ), + enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, ) # Run async embedding in sync context From 01284b92db678a1d358986aaca560d8439f303f9 Mon Sep 17 00:00:00 2001 From: Aleix Dorca Date: Thu, 4 Dec 2025 20:59:20 +0100 Subject: [PATCH 14/71] Update catalan translation.json (#19741) --- src/lib/i18n/locales/ca-ES/translation.json | 78 ++++++++++----------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json index 438c4f6317..fc585804d0 100644 --- a/src/lib/i18n/locales/ca-ES/translation.json +++ b/src/lib/i18n/locales/ca-ES/translation.json @@ -18,15 +18,15 @@ "{{COUNT}} words": "{{COUNT}} paraules", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} a les {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "La descàrrega del model {{model}} s'ha cancel·lat", - "{{NAMES}} reacted with {{REACTION}}": "", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} han reaccionat amb {{REACTION}}", "{{user}}'s Chats": "Els xats de {{user}}", "{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari", "*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges", "1 Source": "1 font", - "A collaboration channel where people join as members": "", - "A discussion channel where access is controlled by groups and permissions": "", + "A collaboration channel where people join as members": "Un canal de col·laboració on la gent s'uneix com a membres", + "A discussion channel where access is controlled by groups and permissions": "Un canal de discussió on l'accés està controlat per grups i permisos", "A new version (v{{LATEST_VERSION}}) is now available.": "Hi ha una nova versió disponible (v{{LATEST_VERSION}}).", - "A private conversation between you and selected users": "", + "A private conversation between you and selected users": "Una conversa privada entre tu i els usuaris seleccionats", "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca per a la web", "a user": "un usuari", "About": "Sobre", @@ -57,8 +57,8 @@ "Add Custom Prompt": "Afegir indicació personalitzada", "Add Details": "Afegir detalls", "Add Files": "Afegir arxius", - "Add Member": "", - "Add Members": "", + "Add Member": "Afegir membre", + "Add Members": "Afegir membres", "Add Memory": "Afegir memòria", "Add Model": "Afegir un model", "Add Reaction": "Afegir reacció", @@ -228,7 +228,7 @@ "Channel deleted successfully": "Canal suprimit correctament", "Channel Name": "Nom del canal", "Channel name cannot be empty.": "El nom del canal no pot estar buit.", - "Channel Type": "", + "Channel Type": "Tipus de canal", "Channel updated successfully": "Canal actualitzat correctament", "Channels": "Canals", "Character": "Personatge", @@ -257,7 +257,7 @@ "Citations": "Cites", "Clear memory": "Esborrar la memòria", "Clear Memory": "Esborrar la memòria", - "Clear status": "", + "Clear status": "Esborrar l'estat", "click here": "prem aquí", "Click here for filter guides.": "Clica aquí per l'ajuda dels filtres.", "Click here for help.": "Clica aquí per obtenir ajuda.", @@ -294,7 +294,7 @@ "Code Interpreter": "Intèrpret de codi", "Code Interpreter Engine": "Motor de l'intèrpret de codi", "Code Interpreter Prompt Template": "Plantilla de la indicació de l'intèrpret de codi", - "Collaboration channel where people join as members": "", + "Collaboration channel where people join as members": "Canal de col·laboració on la gent s'uneix com a membres", "Collapse": "Col·lapsar", "Collection": "Col·lecció", "Color": "Color", @@ -437,7 +437,7 @@ "Direct": "Directe", "Direct Connections": "Connexions directes", "Direct Connections allow users to connect to their own OpenAI compatible API endpoints.": "Les connexions directes permeten als usuaris connectar-se als seus propis endpoints d'API compatibles amb OpenAI.", - "Direct Message": "", + "Direct Message": "Missatge directe", "Direct Tool Servers": "Servidors d'eines directes", "Directory selection was cancelled": "La selecció de directori s'ha cancel·lat", "Disable Code Interpreter": "Deshabilitar l'interpret de codi", @@ -454,7 +454,7 @@ "Discover, download, and explore custom prompts": "Descobrir, descarregar i explorar indicacions personalitzades", "Discover, download, and explore custom tools": "Descobrir, descarregar i explorar eines personalitzades", "Discover, download, and explore model presets": "Descobrir, descarregar i explorar models preconfigurats", - "Discussion channel where access is based on groups and permissions": "", + "Discussion channel where access is based on groups and permissions": "Canal de discussió on l'accés es basa en grups i permisos", "Display": "Mostrar", "Display chat title in tab": "Mostrar el títol del xat a la pestanya", "Display Emoji in Call": "Mostrar emojis a la trucada", @@ -471,7 +471,7 @@ "Document": "Document", "Document Intelligence": "Document Intelligence", "Document Intelligence endpoint required.": "Es necessita un punt de connexió de Document Intelligence", - "Document Intelligence Model": "", + "Document Intelligence Model": "Model de Document Intelligence", "Documentation": "Documentació", "Documents": "Documents", "does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.", @@ -494,15 +494,15 @@ "e.g. \"json\" or a JSON schema": "p. ex. \"json\" o un esquema JSON", "e.g. 60": "p. ex. 60", "e.g. A filter to remove profanity from text": "p. ex. Un filtre per eliminar paraules malsonants del text", - "e.g. about the Roman Empire": "", + "e.g. about the Roman Empire": "p. ex. sobre l'imperi Romà", "e.g. en": "p. ex. en", "e.g. My Filter": "p. ex. El meu filtre", "e.g. My Tools": "p. ex. Les meves eines", "e.g. my_filter": "p. ex. els_meus_filtres", "e.g. my_tools": "p. ex. les_meves_eines", "e.g. pdf, docx, txt": "p. ex. pdf, docx, txt", - "e.g. Tell me a fun fact": "", - "e.g. Tell me a fun fact about the Roman Empire": "", + "e.g. Tell me a fun fact": "p. ex. digues-me quelcome divertit", + "e.g. Tell me a fun fact about the Roman Empire": "p. ex. digues-me quelcom divertit sobre l'imperi Romà", "e.g. Tools for performing various operations": "p. ex. Eines per dur a terme operacions", "e.g., 3, 4, 5 (leave blank for default)": "p. ex. 3, 4, 5 (deixa-ho en blanc per utilitzar el per defecte)", "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "p. ex. audio/wav,audio/mpeg,video/* (deixa-ho en blanc per utilitzar el per defecte)", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "Introdueix la URL del servidor Docling", "Enter Document Intelligence Endpoint": "Introdueix el punt de connexió de Document Intelligence", "Enter Document Intelligence Key": "Introdueix la clau de Document Intelligence", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "Introdueix el model de Document Intelligence", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "Introdueix dominis separats per comes (per exemple, example.com, lloc.org, !excludedsite.com)", "Enter Exa API Key": "Introdueix la clau API de d'EXA", "Enter External Document Loader API Key": "Introdueix la clau API de Document Loader", @@ -716,8 +716,8 @@ "External Web Search URL": "URL d'External Web Search", "Fade Effect for Streaming Text": "Efecte de fos a negre per al text en streaming", "Failed to add file.": "No s'ha pogut afegir l'arxiu.", - "Failed to add members": "", - "Failed to clear status": "", + "Failed to add members": "No s'han pogut afegir el membres", + "Failed to clear status": "No s'ha pogut esborar l'estat", "Failed to connect to {{URL}} OpenAPI tool server": "No s'ha pogut connecta al servidor d'eines OpenAPI {{URL}}", "Failed to copy link": "No s'ha pogut copiar l'enllaç", "Failed to create API Key.": "No s'ha pogut crear la clau API.", @@ -731,14 +731,14 @@ "Failed to load file content.": "No s'ha pogut carregar el contingut del fitxer", "Failed to move chat": "No s'ha pogut moure el xat", "Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls", - "Failed to remove member": "", + "Failed to remove member": "No s'ha pogut eliminar el membre", "Failed to render diagram": "No s'ha pogut renderitzar el diagrama", "Failed to render visualization": "No s'ha pogut renderitzar la visualització", "Failed to save connections": "No s'han pogut desar les connexions", "Failed to save conversation": "No s'ha pogut desar la conversa", "Failed to save models configuration": "No s'ha pogut desar la configuració dels models", "Failed to update settings": "No s'han pogut actualitzar les preferències", - "Failed to update status": "", + "Failed to update status": "No s'ha pogut actualitzar l'estat", "Failed to upload file.": "No s'ha pogut pujar l'arxiu.", "Features": "Característiques", "Features Permissions": "Permisos de les característiques", @@ -832,13 +832,13 @@ "Google PSE Engine Id": "Identificador del motor PSE de Google", "Gravatar": "Gravatar", "Group": "Grup", - "Group Channel": "", + "Group Channel": "Canal de grup", "Group created successfully": "El grup s'ha creat correctament", "Group deleted successfully": "El grup s'ha eliminat correctament", "Group Description": "Descripció del grup", "Group Name": "Nom del grup", "Group updated successfully": "Grup actualitzat correctament", - "groups": "", + "groups": "grups", "Groups": "Grups", "H1": "H1", "H2": "H2", @@ -1028,9 +1028,9 @@ "MCP": "MCP", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "El suport per a MCP és experimental i la seva especificació canvia sovint, cosa que pot provocar incompatibilitats. El suport per a l'especificació d'OpenAPI el manté directament l'equip d'Open WebUI, cosa que el converteix en l'opció més fiable per a la compatibilitat.", "Medium": "Mig", - "Member removed successfully": "", - "Members": "", - "Members added successfully": "", + "Member removed successfully": "El membre s'ha eliminat satisfactòriament", + "Members": "Membres", + "Members added successfully": "Els membres s'han afegit satisfactòriament", "Memories accessible by LLMs will be shown here.": "Les memòries accessibles pels models de llenguatge es mostraran aquí.", "Memory": "Memòria", "Memory added successfully": "Memòria afegida correctament", @@ -1130,7 +1130,7 @@ "No models selected": "No s'ha seleccionat cap model", "No Notes": "No hi ha notes", "No notes found": "No s'han trobat notes", - "No pinned messages": "", + "No pinned messages": "No hi ha missatges fixats", "No prompts found": "No s'han trobat indicacions", "No results": "No s'han trobat resultats", "No results found": "No s'han trobat resultats", @@ -1178,7 +1178,7 @@ "Only alphanumeric characters and hyphens are allowed in the command string.": "Només es permeten caràcters alfanumèrics i guions en la comanda.", "Only can be triggered when the chat input is in focus.": "Només es pot activar quan l'entrada del xat està en focus.", "Only collections can be edited, create a new knowledge base to edit/add documents.": "Només es poden editar col·leccions, crea una nova base de coneixement per editar/afegir documents.", - "Only invited users can access": "", + "Only invited users can access": "Només hi poden accedir els usuaris convidats", "Only markdown files are allowed": "Només es permeten arxius markdown", "Only select users and groups with permission can access": "Només hi poden accedir usuaris i grups seleccionats amb permís", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que la URL no és vàlida. Si us plau, revisa-la i torna-ho a provar.", @@ -1241,7 +1241,7 @@ "Personalization": "Personalització", "Pin": "Fixar", "Pinned": "Fixat", - "Pinned Messages": "", + "Pinned Messages": "Missatges fixats", "Pioneer insights": "Perspectives pioneres", "Pipe": "Canonada", "Pipeline deleted successfully": "Pipeline eliminada correctament", @@ -1271,7 +1271,7 @@ "Please select a model.": "Si us plau, selecciona un model.", "Please select a reason": "Si us plau, selecciona una raó", "Please select a valid JSON file": "Si us plau, selecciona un arxiu JSON vàlid", - "Please select at least one user for Direct Message channel.": "", + "Please select at least one user for Direct Message channel.": "Selecciona com a mínim un usuari per al canal de missatge directe.", "Please wait until all files are uploaded.": "Si us plau, espera fins que s'hagin carregat tots els fitxers.", "Port": "Port", "Positive attitude": "Actitud positiva", @@ -1284,7 +1284,7 @@ "Previous 7 days": "7 dies anteriors", "Previous message": "Missatge anterior", "Private": "Privat", - "Private conversation between selected users": "", + "Private conversation between selected users": "Conversa privada entre usuaris seleccionats", "Profile": "Perfil", "Prompt": "Indicació", "Prompt Autocompletion": "Completar automàticament la indicació", @@ -1473,7 +1473,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Establir el nombre de fils de treball utilitzats per al càlcul. Aquesta opció controla quants fils s'utilitzen per processar les sol·licituds entrants simultàniament. Augmentar aquest valor pot millorar el rendiment amb càrregues de treball de concurrència elevada, però també pot consumir més recursos de CPU.", "Set Voice": "Establir la veu", "Set whisper model": "Establir el model whisper", - "Set your status": "", + "Set your status": "Estableix el teu estat", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Estableix un biaix pla contra tokens que han aparegut almenys una vegada. Un valor més alt (p. ex., 1,5) penalitzarà les repeticions amb més força, mentre que un valor més baix (p. ex., 0,9) serà més indulgent. A 0, està desactivat.", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Estableix un biaix d'escala contra tokens per penalitzar les repeticions, en funció de quantes vegades han aparegut. Un valor més alt (p. ex., 1,5) penalitzarà les repeticions amb més força, mentre que un valor més baix (p. ex., 0,9) serà més indulgent. A 0, està desactivat.", "Sets how far back for the model to look back to prevent repetition.": "Estableix fins a quin punt el model mira enrere per evitar la repetició.", @@ -1526,9 +1526,9 @@ "Start a new conversation": "Iniciar una nova conversa", "Start of the channel": "Inici del canal", "Start Tag": "Etiqueta d'inici", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "Estat", + "Status cleared successfully": "S'ha eliminat correctament el teu estat", + "Status updated successfully": "S'ha actualitzat correctament el teu estat", "Status Updates": "Estat de les actualitzacions", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "Passos", @@ -1544,7 +1544,7 @@ "STT Model": "Model SST", "STT Settings": "Preferències de STT", "Stylized PDF Export": "Exportació en PDF estilitzat", - "Subtitle": "", + "Subtitle": "Subtítol", "Success": "Èxit", "Successfully imported {{userCount}} users.": "S'han importat correctament {{userCount}} usuaris.", "Successfully updated.": "Actualitzat correctament.", @@ -1695,7 +1695,7 @@ "Update and Copy Link": "Actualitzar i copiar l'enllaç", "Update for the latest features and improvements.": "Actualitza per a les darreres característiques i millores.", "Update password": "Actualitzar la contrasenya", - "Update your status": "", + "Update your status": "Actualitza el teu estat", "Updated": "Actualitzat", "Updated at": "Actualitzat el", "Updated At": "Actualitzat el", @@ -1729,7 +1729,7 @@ "User menu": "Menú d'usuari", "User Webhooks": "Webhooks d'usuari", "Username": "Nom d'usuari", - "users": "", + "users": "usuaris", "Users": "Usuaris", "Uses DefaultAzureCredential to authenticate": "Utilitza DefaultAzureCredential per a l'autenticació", "Uses OAuth 2.1 Dynamic Client Registration": "Utilitza el registre dinàmic de clients d'OAuth 2.1", @@ -1749,7 +1749,7 @@ "View Replies": "Veure les respostes", "View Result from **{{NAME}}**": "Veure el resultat de **{{NAME}}**", "Visibility": "Visibilitat", - "Visible to all users": "", + "Visible to all users": "Visible per a tots els usuaris", "Vision": "Visió", "Voice": "Veu", "Voice Input": "Entrada de veu", @@ -1777,7 +1777,7 @@ "What are you trying to achieve?": "Què intentes aconseguir?", "What are you working on?": "En què estàs treballant?", "What's New in": "Què hi ha de nou a", - "What's on your mind?": "", + "What's on your mind?": "Què tens en ment?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Quan està activat, el model respondrà a cada missatge de xat en temps real, generant una resposta tan bon punt l'usuari envia un missatge. Aquest mode és útil per a aplicacions de xat en directe, però pot afectar el rendiment en maquinari més lent.", "wherever you are": "allà on estiguis", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Si es pagina la sortida. Cada pàgina estarà separada per una regla horitzontal i un número de pàgina. Per defecte és Fals.", From 2f68ac33b3c191ba6e61c74afd81e26112af0c70 Mon Sep 17 00:00:00 2001 From: James Westbrook <104535511+westbrook-ai@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:59:30 -0700 Subject: [PATCH 15/71] chore: remove very outdated kubernetes configs (#19731) * cleanup: remove very outdated kubernetes configs * chore: remove INSTALLATION.md that references k8s manifests * chore: remove kubernetes/ from .prettierignore --- .prettierignore | 2 - INSTALLATION.md | 35 ---------------- kubernetes/helm/README.md | 4 -- kubernetes/manifest/base/kustomization.yaml | 8 ---- kubernetes/manifest/base/ollama-service.yaml | 12 ------ .../manifest/base/ollama-statefulset.yaml | 41 ------------------- kubernetes/manifest/base/open-webui.yaml | 4 -- .../manifest/base/webui-deployment.yaml | 38 ----------------- kubernetes/manifest/base/webui-ingress.yaml | 20 --------- kubernetes/manifest/base/webui-pvc.yaml | 12 ------ kubernetes/manifest/base/webui-service.yaml | 15 ------- kubernetes/manifest/gpu/kustomization.yaml | 8 ---- .../manifest/gpu/ollama-statefulset-gpu.yaml | 17 -------- 13 files changed, 216 deletions(-) delete mode 100644 INSTALLATION.md delete mode 100644 kubernetes/helm/README.md delete mode 100644 kubernetes/manifest/base/kustomization.yaml delete mode 100644 kubernetes/manifest/base/ollama-service.yaml delete mode 100644 kubernetes/manifest/base/ollama-statefulset.yaml delete mode 100644 kubernetes/manifest/base/open-webui.yaml delete mode 100644 kubernetes/manifest/base/webui-deployment.yaml delete mode 100644 kubernetes/manifest/base/webui-ingress.yaml delete mode 100644 kubernetes/manifest/base/webui-pvc.yaml delete mode 100644 kubernetes/manifest/base/webui-service.yaml delete mode 100644 kubernetes/manifest/gpu/kustomization.yaml delete mode 100644 kubernetes/manifest/gpu/ollama-statefulset-gpu.yaml diff --git a/.prettierignore b/.prettierignore index 82c4912572..83bbde598b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,8 +3,6 @@ pnpm-lock.yaml package-lock.json yarn.lock -kubernetes/ - # Copy of .gitignore .DS_Store node_modules diff --git a/INSTALLATION.md b/INSTALLATION.md deleted file mode 100644 index 4298b173e9..0000000000 --- a/INSTALLATION.md +++ /dev/null @@ -1,35 +0,0 @@ -### Installing Both Ollama and Open WebUI Using Kustomize - -For cpu-only pod - -```bash -kubectl apply -f ./kubernetes/manifest/base -``` - -For gpu-enabled pod - -```bash -kubectl apply -k ./kubernetes/manifest -``` - -### Installing Both Ollama and Open WebUI Using Helm - -Package Helm file first - -```bash -helm package ./kubernetes/helm/ -``` - -For cpu-only pod - -```bash -helm install ollama-webui ./ollama-webui-*.tgz -``` - -For gpu-enabled pod - -```bash -helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1" -``` - -Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization diff --git a/kubernetes/helm/README.md b/kubernetes/helm/README.md deleted file mode 100644 index 5737007d96..0000000000 --- a/kubernetes/helm/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Helm Charts -Open WebUI Helm Charts are now hosted in a separate repo, which can be found here: https://github.com/open-webui/helm-charts - -The charts are released at https://helm.openwebui.com. \ No newline at end of file diff --git a/kubernetes/manifest/base/kustomization.yaml b/kubernetes/manifest/base/kustomization.yaml deleted file mode 100644 index 61500f87c5..0000000000 --- a/kubernetes/manifest/base/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -resources: - - open-webui.yaml - - ollama-service.yaml - - ollama-statefulset.yaml - - webui-deployment.yaml - - webui-service.yaml - - webui-ingress.yaml - - webui-pvc.yaml diff --git a/kubernetes/manifest/base/ollama-service.yaml b/kubernetes/manifest/base/ollama-service.yaml deleted file mode 100644 index 8bab65b59e..0000000000 --- a/kubernetes/manifest/base/ollama-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: ollama-service - namespace: open-webui -spec: - selector: - app: ollama - ports: - - protocol: TCP - port: 11434 - targetPort: 11434 \ No newline at end of file diff --git a/kubernetes/manifest/base/ollama-statefulset.yaml b/kubernetes/manifest/base/ollama-statefulset.yaml deleted file mode 100644 index cd1144caf9..0000000000 --- a/kubernetes/manifest/base/ollama-statefulset.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: ollama - namespace: open-webui -spec: - serviceName: "ollama" - replicas: 1 - selector: - matchLabels: - app: ollama - template: - metadata: - labels: - app: ollama - spec: - containers: - - name: ollama - image: ollama/ollama:latest - ports: - - containerPort: 11434 - resources: - requests: - cpu: "2000m" - memory: "2Gi" - limits: - cpu: "4000m" - memory: "4Gi" - nvidia.com/gpu: "0" - volumeMounts: - - name: ollama-volume - mountPath: /root/.ollama - tty: true - volumeClaimTemplates: - - metadata: - name: ollama-volume - spec: - accessModes: [ "ReadWriteOnce" ] - resources: - requests: - storage: 30Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/open-webui.yaml b/kubernetes/manifest/base/open-webui.yaml deleted file mode 100644 index 9c1a599f32..0000000000 --- a/kubernetes/manifest/base/open-webui.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: open-webui \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml deleted file mode 100644 index 79a0a9a23c..0000000000 --- a/kubernetes/manifest/base/webui-deployment.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: open-webui-deployment - namespace: open-webui -spec: - replicas: 1 - selector: - matchLabels: - app: open-webui - template: - metadata: - labels: - app: open-webui - spec: - containers: - - name: open-webui - image: ghcr.io/open-webui/open-webui:main - ports: - - containerPort: 8080 - resources: - requests: - cpu: "500m" - memory: "500Mi" - limits: - cpu: "1000m" - memory: "1Gi" - env: - - name: OLLAMA_BASE_URL - value: "http://ollama-service.open-webui.svc.cluster.local:11434" - tty: true - volumeMounts: - - name: webui-volume - mountPath: /app/backend/data - volumes: - - name: webui-volume - persistentVolumeClaim: - claimName: open-webui-pvc \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-ingress.yaml b/kubernetes/manifest/base/webui-ingress.yaml deleted file mode 100644 index dc0b53ccd4..0000000000 --- a/kubernetes/manifest/base/webui-ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: open-webui-ingress - namespace: open-webui - #annotations: - # Use appropriate annotations for your Ingress controller, e.g., for NGINX: - # nginx.ingress.kubernetes.io/rewrite-target: / -spec: - rules: - - host: open-webui.minikube.local - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: open-webui-service - port: - number: 8080 diff --git a/kubernetes/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml deleted file mode 100644 index 97fb761d42..0000000000 --- a/kubernetes/manifest/base/webui-pvc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - app: open-webui - name: open-webui-pvc - namespace: open-webui -spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 2Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-service.yaml b/kubernetes/manifest/base/webui-service.yaml deleted file mode 100644 index d73845f00a..0000000000 --- a/kubernetes/manifest/base/webui-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: open-webui-service - namespace: open-webui -spec: - type: NodePort # Use LoadBalancer if you're on a cloud that supports it - selector: - app: open-webui - ports: - - protocol: TCP - port: 8080 - targetPort: 8080 - # If using NodePort, you can optionally specify the nodePort: - # nodePort: 30000 \ No newline at end of file diff --git a/kubernetes/manifest/gpu/kustomization.yaml b/kubernetes/manifest/gpu/kustomization.yaml deleted file mode 100644 index c0d39fbfaa..0000000000 --- a/kubernetes/manifest/gpu/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../base - -patches: -- path: ollama-statefulset-gpu.yaml diff --git a/kubernetes/manifest/gpu/ollama-statefulset-gpu.yaml b/kubernetes/manifest/gpu/ollama-statefulset-gpu.yaml deleted file mode 100644 index 3e42443656..0000000000 --- a/kubernetes/manifest/gpu/ollama-statefulset-gpu.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: ollama - namespace: open-webui -spec: - selector: - matchLabels: - app: ollama - serviceName: "ollama" - template: - spec: - containers: - - name: ollama - resources: - limits: - nvidia.com/gpu: "1" From d90d036d23749f21b92eadd81890e09bb7415f1e Mon Sep 17 00:00:00 2001 From: _00_ <131402327+rgaricano@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:00:15 +0100 Subject: [PATCH 16/71] UPD: i18n es-ES Translation v.0.6.41 (#19745) ### UPD: i18n es-ES Translation v.0.6.41 Added new strings Co-authored-by: Tim Baek --- src/lib/i18n/locales/es-ES/translation.json | 120 ++++++++++---------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json index dc9a7c6a4b..448805ed79 100644 --- a/src/lib/i18n/locales/es-ES/translation.json +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -18,15 +18,15 @@ "{{COUNT}} words": "{{COUNT}} palabras", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} a las {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "La descarga de {{model}} ha sido cancelada", - "{{NAMES}} reacted with {{REACTION}}": "", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} han reaccionado con {{REACTION}}", "{{user}}'s Chats": "Chats de {{user}}", "{{webUIName}} Backend Required": "{{webUIName}} Servidor Requerido", "*Prompt node ID(s) are required for image generation": "Los ID de nodo son requeridos para la generación de imágenes", "1 Source": "1 Fuente", - "A collaboration channel where people join as members": "", - "A discussion channel where access is controlled by groups and permissions": "", + "A collaboration channel where people join as members": "Canal colaborativo donde la gente se une como miembro", + "A discussion channel where access is controlled by groups and permissions": "Un canal de discusión con el acceso controlado mediante grupos y permisos", "A new version (v{{LATEST_VERSION}}) is now available.": "Nueva versión (v{{LATEST_VERSION}}) disponible.", - "A private conversation between you and selected users": "", + "A private conversation between you and selected users": "conversación privada entre el usuario seleccionado y tu", "A task model is used when performing tasks such as generating titles for chats and web search queries": "El modelo de tareas realiza tareas como la generación de títulos para chats y consultas de búsqueda web", "a user": "un usuario", "About": "Acerca de", @@ -57,8 +57,8 @@ "Add Custom Prompt": "Añadir Indicador Personalizado", "Add Details": "Añadir Detalles", "Add Files": "Añadir Archivos", - "Add Member": "", - "Add Members": "", + "Add Member": "Añadir Miembro", + "Add Members": "Añadir Miembros", "Add Memory": "Añadir Memoria", "Add Model": "Añadir Modelo", "Add Reaction": "Añadir Reacción", @@ -99,7 +99,7 @@ "Allow Continue Response": "Permitir Continuar Respuesta", "Allow Delete Messages": "Permitir Borrar Mensajes", "Allow File Upload": "Permitir Subida de Archivos", - "Allow Group Sharing": "", + "Allow Group Sharing": "Permitir Compartir en Grupo", "Allow Multiple Models in Chat": "Permitir Chat con Múltiples Modelos", "Allow non-local voices": "Permitir voces no locales", "Allow Rate Response": "Permitir Calificar Respuesta", @@ -147,7 +147,7 @@ "Archived Chats": "Chats archivados", "archived-chat-export": "exportar chats archivados", "Are you sure you want to clear all memories? This action cannot be undone.": "¿Segur@ de que quieres borrar todas las memorias? (¡esta acción NO se puede deshacer!)", - "Are you sure you want to delete \"{{NAME}}\"?": "", + "Are you sure you want to delete \"{{NAME}}\"?": "¿Segur@ de que quieres eliminar \"{{NAME}}\"?", "Are you sure you want to delete this channel?": "¿Segur@ de que quieres eliminar este canal?", "Are you sure you want to delete this message?": "¿Segur@ de que quieres eliminar este mensaje? ", "Are you sure you want to unarchive all archived chats?": "¿Segur@ de que quieres desarchivar todos los chats archivados?", @@ -157,7 +157,7 @@ "Ask": "Preguntar", "Ask a question": "Haz una pregunta", "Assistant": "Asistente", - "Async Embedding Processing": "", + "Async Embedding Processing": "Procesado Asíncrono al Incrustrar", "Attach File From Knowledge": "Adjuntar Archivo desde Conocimiento", "Attach Knowledge": "Adjuntar Conocimiento", "Attach Notes": "Adjuntar Notas", @@ -228,7 +228,7 @@ "Channel deleted successfully": "Canal borrado correctamente", "Channel Name": "Nombre del Canal", "Channel name cannot be empty.": "El nombre del Canal no puede estar vacío", - "Channel Type": "", + "Channel Type": "Tipo de Canal", "Channel updated successfully": "Canal actualizado correctamente", "Channels": "Canal", "Character": "Carácter", @@ -257,7 +257,7 @@ "Citations": "Citas", "Clear memory": "Liberar memoria", "Clear Memory": "Liberar Memoria", - "Clear status": "", + "Clear status": "Limpiar estado", "click here": "Pulsar aquí", "Click here for filter guides.": "Pulsar aquí para guías de filtros", "Click here for help.": "Pulsar aquí para Ayuda.", @@ -294,7 +294,7 @@ "Code Interpreter": "Interprete de Código", "Code Interpreter Engine": "Motor del Interprete de Código", "Code Interpreter Prompt Template": "Plantilla del Indicador del Interprete de Código", - "Collaboration channel where people join as members": "", + "Collaboration channel where people join as members": "Canal de colaboración donde la gente se une como miembro", "Collapse": "Plegar", "Collection": "Colección", "Color": "Color", @@ -395,14 +395,14 @@ "Default description enabled": "Descripción predeterminada activada", "Default Features": "Características Predeterminadas", "Default Filters": "Filtros Predeterminados", - "Default Group": "", + "Default Group": "Grupo Predeterminado", "Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature.": "El modo predeterminado trabaja con un amplio rango de modelos, llamando a las herramientas una vez antes de la ejecución. El modo nativo aprovecha las capacidades de llamada de herramientas integradas del modelo, pero requiere que el modelo admita inherentemente esta función.", "Default Model": "Modelo Predeterminado", "Default model updated": "El modelo Predeterminado ha sido actualizado", "Default Models": "Modelos Predeterminados", "Default permissions": "Permisos Predeterminados", "Default permissions updated successfully": "Permisos predeterminados actualizados correctamente", - "Default Pinned Models": "", + "Default Pinned Models": "Modelos Fijados Predeterminados", "Default Prompt Suggestions": "Sugerencias Predeterminadas de Indicador", "Default to 389 or 636 if TLS is enabled": "Predeterminado a 389, o 636 si TLS está habilitado", "Default to ALL": "Predeterminado a TODOS", @@ -411,7 +411,7 @@ "Delete": "Borrar", "Delete a model": "Borrar un modelo", "Delete All Chats": "Borrar todos los chats", - "Delete all contents inside this folder": "", + "Delete all contents inside this folder": "Borrar todo el contenido de esta carpeta", "Delete All Models": "Borrar todos los modelos", "Delete Chat": "Borrar Chat", "Delete chat?": "¿Borrar el chat?", @@ -437,7 +437,7 @@ "Direct": "Directo", "Direct Connections": "Conexiones Directas", "Direct Connections allow users to connect to their own OpenAI compatible API endpoints.": "Las Conexiones Directas permiten a los usuarios conectar a sus propios endpoints compatibles API OpenAI.", - "Direct Message": "", + "Direct Message": "Mensaje Directo", "Direct Tool Servers": "Servidores de Herramientas Directos", "Directory selection was cancelled": "La selección de directorio ha sido cancelada", "Disable Code Interpreter": "Deshabilitar Interprete de Código", @@ -454,7 +454,7 @@ "Discover, download, and explore custom prompts": "Descubre, descarga, y explora indicadores personalizados", "Discover, download, and explore custom tools": "Descubre, descarga y explora herramientas personalizadas", "Discover, download, and explore model presets": "Descubre, descarga y explora modelos con preajustados", - "Discussion channel where access is based on groups and permissions": "", + "Discussion channel where access is based on groups and permissions": "Canal de discusión con acceso basado en grupos y permisos", "Display": "Mostrar", "Display chat title in tab": "Mostrar título del chat en el tabulador", "Display Emoji in Call": "Muestra Emojis en Llamada", @@ -471,7 +471,7 @@ "Document": "Documento", "Document Intelligence": "Azure Doc Intelligence", "Document Intelligence endpoint required.": "Endpoint Azure Doc Intelligence requerido", - "Document Intelligence Model": "", + "Document Intelligence Model": "Modelo para Doc Intelligence", "Documentation": "Documentación", "Documents": "Documentos", "does not make any external connections, and your data stays securely on your locally hosted server.": "no se realiza ninguna conexión externa y tus datos permanecen seguros alojados localmente en tu servidor.", @@ -494,15 +494,15 @@ "e.g. \"json\" or a JSON schema": "p.ej. \"json\" o un esquema JSON", "e.g. 60": "p.ej. 60", "e.g. A filter to remove profanity from text": "p.ej. Un filtro para eliminar malas palabras del texto", - "e.g. about the Roman Empire": "", + "e.g. about the Roman Empire": "p.ej. sobre el imperio romano", "e.g. en": "p.ej. es", "e.g. My Filter": "p.ej. Mi Filtro", "e.g. My Tools": "p.ej. Mis Herramientas", "e.g. my_filter": "p.ej. mi_filtro", "e.g. my_tools": "p.ej. mis_herramientas", "e.g. pdf, docx, txt": "p.ej. pdf, docx, txt ...", - "e.g. Tell me a fun fact": "", - "e.g. Tell me a fun fact about the Roman Empire": "", + "e.g. Tell me a fun fact": "p.ej. Dime algo divertido", + "e.g. Tell me a fun fact about the Roman Empire": "p.ej. Dime algo divertido sobre el imperio romano", "e.g. Tools for performing various operations": "p.ej. Herramientas para realizar diversas operaciones", "e.g., 3, 4, 5 (leave blank for default)": "p.ej. , 3, 4, 5 ...", "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "e.g., audio/wav,audio/mpeg,video/* (dejar en blanco para predeterminados)", @@ -572,11 +572,11 @@ "Enter Datalab Marker API Base URL": "Ingresar la URL Base para la API de Datalab Marker", "Enter Datalab Marker API Key": "Ingresar Clave API de Datalab Marker", "Enter description": "Ingresar Descripción", - "Enter Docling API Key": "", + "Enter Docling API Key": "Ingresar Clave API de Docling", "Enter Docling Server URL": "Ingresar URL del Servidor Docling", "Enter Document Intelligence Endpoint": "Ingresar el Endpoint de Azure Document Intelligence", "Enter Document Intelligence Key": "Ingresar Clave de Azure Document Intelligence", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "Ingresar Modelo para Azure Document Intelligence", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "Ingresar dominios separados por comas (ej. ejemplocom, site,org, !excludedsite.com)", "Enter Exa API Key": "Ingresar Clave API de Exa", "Enter External Document Loader API Key": "Ingresar Clave API del Cargador Externo de Documentos", @@ -588,7 +588,7 @@ "Enter Firecrawl API Base URL": "Ingresar URL Base del API de Firecrawl", "Enter Firecrawl API Key": "Ingresar Clave del API de Firecrawl", "Enter folder name": "Ingresar nombre de la carpeta", - "Enter function name filter list (e.g. func1, !func2)": "", + "Enter function name filter list (e.g. func1, !func2)": "Ingresar lista para el filtro de nombres de función (p.ej.: func1, !func2)", "Enter Github Raw URL": "Ingresar URL Github en Bruto(raw)", "Enter Google PSE API Key": "Ingresar Clave API de Google PSE", "Enter Google PSE Engine Id": "Ingresa ID del Motor PSE de Google", @@ -716,8 +716,8 @@ "External Web Search URL": "URL del Buscador Web Externo", "Fade Effect for Streaming Text": "Efecto de desvanecimiento para texto transmitido (streaming)", "Failed to add file.": "Fallo al añadir el archivo.", - "Failed to add members": "", - "Failed to clear status": "", + "Failed to add members": "Fallo al añadir miembros", + "Failed to clear status": "Fallo al limpiar el estado", "Failed to connect to {{URL}} OpenAPI tool server": "Fallo al conectar al servidor de herramientas {{URL}}", "Failed to copy link": "Fallo al copiar enlace", "Failed to create API Key.": "Fallo al crear la Clave API.", @@ -731,19 +731,19 @@ "Failed to load file content.": "Fallo al cargar el contenido del archivo", "Failed to move chat": "Fallo al mover el chat", "Failed to read clipboard contents": "Fallo al leer el contenido del portapapeles", - "Failed to remove member": "", + "Failed to remove member": "Fallo al eliminar miembro", "Failed to render diagram": "Fallo al renderizar el diagrama", "Failed to render visualization": "Fallo al renderizar la visualización", "Failed to save connections": "Fallo al guardar las conexiones", "Failed to save conversation": "Fallo al guardar la conversación", "Failed to save models configuration": "Fallo al guardar la configuración de los modelos", "Failed to update settings": "Fallo al actualizar los ajustes", - "Failed to update status": "", + "Failed to update status": "Fallo al actualizar el estado", "Failed to upload file.": "Fallo al subir el archivo.", "Features": "Características", "Features Permissions": "Permisos de las Características", "February": "Febrero", - "Feedback deleted successfully": "", + "Feedback deleted successfully": "Opinión eliminada correctamente", "Feedback Details": "Detalle de la Opinión", "Feedback History": "Historia de la Opiniones", "Feedbacks": "Opiniones", @@ -758,7 +758,7 @@ "File size should not exceed {{maxSize}} MB.": "Tamaño del archivo no debe exceder {{maxSize}} MB.", "File Upload": "Subir Archivo", "File uploaded successfully": "Archivo subido correctamente", - "File uploaded!": "", + "File uploaded!": "¡Archivo subido!", "Files": "Archivos", "Filter": "Filtro", "Filter is now globally disabled": "El filtro ahora está desactivado globalmente", @@ -803,7 +803,7 @@ "Function is now globally disabled": "La Función ahora está deshabilitada globalmente", "Function is now globally enabled": "La Función ahora está habilitada globalmente", "Function Name": "Nombre de la Función", - "Function Name Filter List": "", + "Function Name Filter List": "Lista del Filtro de Nombres de Función", "Function updated successfully": "Función actualizada correctamente", "Functions": "Funciones", "Functions allow arbitrary code execution.": "Las Funciones habilitan la ejecución de código arbitrario.", @@ -832,13 +832,13 @@ "Google PSE Engine Id": "ID del Motor PSE de Google", "Gravatar": "Gravatar", "Group": "Grupo", - "Group Channel": "", + "Group Channel": "Canal de Grupo", "Group created successfully": "Grupo creado correctamente", "Group deleted successfully": "Grupo eliminado correctamente", "Group Description": "Descripción del Grupo", "Group Name": "Nombre del Grupo", "Group updated successfully": "Grupo actualizado correctamente", - "groups": "", + "groups": "grupos", "Groups": "Grupos", "H1": "H1", "H2": "H2", @@ -875,7 +875,7 @@ "Image Compression": "Compresión de Imagen", "Image Compression Height": "Alto en Compresión de Imagen", "Image Compression Width": "Ancho en Compresión de Imagen", - "Image Edit": "", + "Image Edit": "Editar Imagen", "Image Edit Engine": "Motor del Editor de Imágenes", "Image Generation": "Generación de Imagen", "Image Generation Engine": "Motor de Generación de Imagen", @@ -958,7 +958,7 @@ "Knowledge Name": "Nombre del Conocimiento", "Knowledge Public Sharing": "Compartir Conocimiento Públicamente", "Knowledge reset successfully.": "Conocimiento restablecido correctamente.", - "Knowledge Sharing": "", + "Knowledge Sharing": "Compartir Conocimiento", "Knowledge updated successfully": "Conocimiento actualizado correctamente.", "Kokoro.js (Browser)": "Kokoro.js (Navegador)", "Kokoro.js Dtype": "Kokoro.js DType", @@ -1024,13 +1024,13 @@ "Max Upload Size": "Tamaño Max de Subidas", "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Se puede descargar un máximo de 3 modelos simultáneamente. Por favor, reinténtelo más tarde.", "May": "Mayo", - "MBR": "", + "MBR": "MBR", "MCP": "MCP", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "El soporte de MCP es experimental y su especificación cambia con frecuencia, lo que puede generar incompatibilidades. El equipo de Open WebUI mantiene directamente la compatibilidad con la especificación OpenAPI, lo que la convierte en la opción más fiable para la compatibilidad.", "Medium": "Medio", - "Member removed successfully": "", - "Members": "", - "Members added successfully": "", + "Member removed successfully": "Miembro removido correctamente", + "Members": "Miembros", + "Members added successfully": "Miembros añadidos correctamente", "Memories accessible by LLMs will be shown here.": "Las memorias accesibles por los LLMs se mostrarán aquí.", "Memory": "Memoria", "Memory added successfully": "Memoria añadida correctamente", @@ -1084,7 +1084,7 @@ "Models configuration saved successfully": "Configuración de Modelos guardada correctamente", "Models imported successfully": "Modelos importados correctamente", "Models Public Sharing": "Compartir Modelos Públicamente", - "Models Sharing": "", + "Models Sharing": "Compartir Modelos", "Mojeek Search API Key": "Clave API de Mojeek Search", "More": "Más", "More Concise": "Más Conciso", @@ -1130,7 +1130,7 @@ "No models selected": "No se seleccionaron modelos", "No Notes": "Sin Notas", "No notes found": "No se encontraron notas", - "No pinned messages": "", + "No pinned messages": "No hay mensajes fijados", "No prompts found": "No se encontraron indicadores", "No results": "No se encontraron resultados", "No results found": "No se encontraron resultados", @@ -1152,7 +1152,7 @@ "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si estableces una puntuación mínima, la búsqueda sólo devolverá documentos con una puntuación mayor o igual a la puntuación mínima establecida.", "Notes": "Notas", "Notes Public Sharing": "Compartir Notas Publicamente", - "Notes Sharing": "", + "Notes Sharing": "Compartir Notas", "Notification Sound": "Notificación Sonora", "Notification Webhook": "Notificación Enganchada (webhook)", "Notifications": "Notificaciones", @@ -1178,7 +1178,7 @@ "Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo están permitidos en la cadena de comandos caracteres alfanuméricos y guiones.", "Only can be triggered when the chat input is in focus.": "Solo se puede activar cuando el foco está en la entrada del chat.", "Only collections can be edited, create a new knowledge base to edit/add documents.": "Solo se pueden editar las colecciones, para añadir/editar documentos hay que crear una nueva base de conocimientos", - "Only invited users can access": "", + "Only invited users can access": "Solo pueden acceder usuarios invitados", "Only markdown files are allowed": "Solo están permitidos archivos markdown", "Only select users and groups with permission can access": "Solo pueden acceder los usuarios y grupos con permiso", "Oops! Looks like the URL is invalid. Please double-check and try again.": "¡vaya! Parece que la URL es inválida. Por favor, revisala y reintenta de nuevo.", @@ -1241,7 +1241,7 @@ "Personalization": "Personalización", "Pin": "Fijar", "Pinned": "Fijado", - "Pinned Messages": "", + "Pinned Messages": "Mensajes Fijados", "Pioneer insights": "Descubrir nuevas perspectivas", "Pipe": "Tubo", "Pipeline deleted successfully": "Tubería borrada correctamente", @@ -1271,7 +1271,7 @@ "Please select a model.": "Por favor selecciona un modelo.", "Please select a reason": "Por favor selecciona un motivo", "Please select a valid JSON file": "Por favor selecciona un fichero JSON válido", - "Please select at least one user for Direct Message channel.": "", + "Please select at least one user for Direct Message channel.": "Por favor selecciona al menos un usuario para el canal de Mensajes Directos", "Please wait until all files are uploaded.": "Por favor, espera a que todos los ficheros se acaben de subir", "Port": "Puerto", "Positive attitude": "Actitud Positiva", @@ -1284,7 +1284,7 @@ "Previous 7 days": "7 días previos", "Previous message": "Mensaje anterior", "Private": "Privado", - "Private conversation between selected users": "", + "Private conversation between selected users": "Conversación privada entre l@s usuari@s seleccionados", "Profile": "Perfil", "Prompt": "Indicador", "Prompt Autocompletion": "Autocompletado del Indicador", @@ -1294,7 +1294,7 @@ "Prompts": "Indicadores", "Prompts Access": "Acceso a Indicadores", "Prompts Public Sharing": "Compartir Indicadores Públicamente", - "Prompts Sharing": "", + "Prompts Sharing": "Compartir Indicadores", "Provider Type": "Tipo de Proveedor", "Public": "Público", "Pull \"{{searchValue}}\" from Ollama.com": "Extraer \"{{searchValue}}\" desde Ollama.com", @@ -1378,7 +1378,7 @@ "Run": "Ejecutar", "Running": "Ejecutando", "Running...": "Ejecutando...", - "Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.": "", + "Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.": "Ejecuta tareas de incrustración concurrentes para acelerar el procesado. Desactivar si se generan problemas (por limitaciones de los motores de incrustracción en uso)", "Save": "Guardar", "Save & Create": "Guardar y Crear", "Save & Update": "Guardar y Actualizar", @@ -1473,14 +1473,14 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Establece el número de hilos de trabajo utilizados para el computo. Esta opción controla cuántos hilos son usados para procesar solicitudes entrantes concurrentes. Aumentar este valor puede mejorar el rendimiento bajo cargas de trabajo de alta concurrencia, pero también puede consumir más recursos de la CPU.", "Set Voice": "Establecer la voz", "Set whisper model": "Establecer modelo whisper (transcripción)", - "Set your status": "", + "Set your status": "Establece tu estado", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Establece un sesgo plano contra los tokens que han aparecido al menos una vez. Un valor más alto (p.ej. 1.5) penalizará las repeticiones más fuertemente, mientras que un valor más bajo (p.ej. 0.9) será más indulgente. En 0, está deshabilitado.", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Establece un sesgo escalado contra los tokens para penalizar las repeticiones, basado en cuántas veces han aparecido. Un valor más alto (por ejemplo, 1.5) penalizará las repeticiones más fuertemente, mientras que un valor más bajo (por ejemplo, 0.9) será más indulgente. En 0, está deshabilitado.", "Sets how far back for the model to look back to prevent repetition.": "Establece cuántos tokens debe mirar atrás el modelo para prevenir la repetición. ", "Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt.": "Establece la semilla de números aleatorios a usar para la generación. Establecer esto en un número específico hará que el modelo genere el mismo texto para el mismo indicador(prompt).", "Sets the size of the context window used to generate the next token.": "Establece el tamaño de la ventana del contexto utilizada para generar el siguiente token.", "Sets the stop sequences to use. When this pattern is encountered, the LLM will stop generating text and return. Multiple stop patterns may be set by specifying multiple separate stop parameters in a modelfile.": "Establece las secuencias de parada a usar. Cuando se encuentre este patrón, el LLM dejará de generar texto y retornará. Se pueden establecer varios patrones de parada especificando separadamente múltiples parámetros de parada en un archivo de modelo.", - "Setting": "", + "Setting": "Ajuste", "Settings": "Ajustes", "Settings saved successfully!": "¡Ajustes guardados correctamente!", "Share": "Compartir", @@ -1526,9 +1526,9 @@ "Start a new conversation": "Comenzar una conversación nueva", "Start of the channel": "Inicio del canal", "Start Tag": "Etiqueta de Inicio", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "Estado", + "Status cleared successfully": "Estado limpiado correctamente", + "Status updated successfully": "Estado actualizado correctamente", "Status Updates": "Actualizaciones de Estado", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "Pasos", @@ -1544,7 +1544,7 @@ "STT Model": "Modelo STT", "STT Settings": "Ajustes Voz a Texto (STT)", "Stylized PDF Export": "Exportar PDF Estilizado", - "Subtitle": "", + "Subtitle": "Subtítulo", "Success": "Correcto", "Successfully imported {{userCount}} users.": "{{userCount}} usuarios importados correctamente.", "Successfully updated.": "Actualizado correctamente.", @@ -1661,7 +1661,7 @@ "Tools Function Calling Prompt": "Indicador para la Función de Llamada a las Herramientas", "Tools have a function calling system that allows arbitrary code execution.": "Las herramientas tienen un sistema de llamada de funciones que permite la ejecución de código arbitrario.", "Tools Public Sharing": "Compartir Herramientas Publicamente", - "Tools Sharing": "", + "Tools Sharing": "Compartir Herramientas", "Top K": "Top K", "Top K Reranker": "Top K Reclasificador", "Transformers": "Transformadores", @@ -1695,7 +1695,7 @@ "Update and Copy Link": "Actualizar y Copiar Enlace", "Update for the latest features and improvements.": "Actualizar para las últimas características y mejoras.", "Update password": "Actualizar contraseña", - "Update your status": "", + "Update your status": "Actualizar tu estado", "Updated": "Actualizado", "Updated at": "Actualizado el", "Updated At": "Actualizado El", @@ -1710,7 +1710,7 @@ "Upload Pipeline": "Subir Tubería", "Upload Progress": "Progreso de la Subida", "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "Progreso de la Subida: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", - "Uploading file...": "", + "Uploading file...": "Subiendo archivo...", "URL": "URL", "URL is required": "La URL es requerida", "URL Mode": "Modo URL", @@ -1729,7 +1729,7 @@ "User menu": "Menu de Usuario", "User Webhooks": "Usuario Webhooks", "Username": "Nombre de Usuario", - "users": "", + "users": "usuarios", "Users": "Usuarios", "Uses DefaultAzureCredential to authenticate": "Usa DefaultAzureCredential para autentificar", "Uses OAuth 2.1 Dynamic Client Registration": "Usa Registro dinámico de cliente OAuth 2.1", @@ -1749,7 +1749,7 @@ "View Replies": "Ver Respuestas", "View Result from **{{NAME}}**": "Ver Resultado desde **{{NAME}}**", "Visibility": "Visibilidad", - "Visible to all users": "", + "Visible to all users": "Visible para todos los usuarios", "Vision": "Visión", "Voice": "Voz", "Voice Input": "Entrada de Voz", @@ -1777,7 +1777,7 @@ "What are you trying to achieve?": "¿Qué estás tratando de conseguir?", "What are you working on?": "¿En qué estás trabajando?", "What's New in": "Que hay de Nuevo en", - "What's on your mind?": "", + "What's on your mind?": "¿En que estás pensando?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Cuando está habilitado, el modelo responderá a cada mensaje de chat en tiempo real, generando una respuesta tan pronto como se envíe un mensaje. Este modo es útil para aplicaciones de chat en vivo, pero puede afectar al rendimiento en equipos más lentos.", "wherever you are": "dondequiera que estés", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Al paginar la salida. Cada página será separada por una línea horizontal y número de página. Por defecto: Falso", From 2a8a2f1ba34f5a75b23377eaf803fc6aeb4f320a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 4 Dec 2025 20:40:52 -0500 Subject: [PATCH 17/71] refac --- src/lib/components/common/DragGhost.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/components/common/DragGhost.svelte b/src/lib/components/common/DragGhost.svelte index 515c91c1c8..526a66ae2e 100644 --- a/src/lib/components/common/DragGhost.svelte +++ b/src/lib/components/common/DragGhost.svelte @@ -12,7 +12,14 @@ }); onDestroy(() => { - document.body.removeChild(popupElement); + if (popupElement && popupElement.parentNode) { + try { + popupElement.parentNode.removeChild(popupElement); + } catch (err) { + console.warn('Failed to remove popupElement:', err); + } + } + document.body.style.overflow = 'unset'; }); From 508fde70ff35edf4342445b9c67d191a1290c32d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Dec 2025 08:02:12 -0500 Subject: [PATCH 18/71] fix: temp chat perm --- src/lib/components/chat/Chat.svelte | 11 +++++-- .../components/chat/Settings/Interface.svelte | 32 ++++++++++--------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 7f80bca601..c2ebe59bea 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -902,8 +902,15 @@ const initNewChat = async () => { console.log('initNewChat'); - if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) { - await temporaryChatEnabled.set(true); + if ($user?.role !== 'admin') { + if ($user?.permissions?.chat?.temporary_enforced) { + await temporaryChatEnabled.set(true); + } + + if (!$user?.permissions?.chat?.temporary) { + await temporaryChatEnabled.set(false); + return; + } } if ($settings?.temporaryChatByDefault ?? false) { diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index b905e09b11..14b84aab58 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -713,24 +713,26 @@
-
-
-
- {$i18n.t('Temporary Chat by Default')} -
+ {#if $user.role === 'admin' || $user?.permissions?.chat?.temporary} +
+
+
+ {$i18n.t('Temporary Chat by Default')} +
-
- { - saveSettings({ temporaryChatByDefault }); - }} - /> +
+ { + saveSettings({ temporaryChatByDefault }); + }} + /> +
-
+ {/if}
From cdd75ade50a4403ed0aa834f9610eb84677e71b9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:03:04 +0100 Subject: [PATCH 19/71] BREAKING/CAUTION: chore: chromadb dep bump - needs testing (#19780) * Update requirements.txt * Update requirements-min.txt * Update pyproject.toml --- backend/requirements-min.txt | 2 +- backend/requirements.txt | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt index f22ad7f0cf..714d44c1bd 100644 --- a/backend/requirements-min.txt +++ b/backend/requirements-min.txt @@ -45,7 +45,7 @@ langchain==0.3.27 langchain-community==0.3.29 fake-useragent==2.2.0 -chromadb==1.1.0 +chromadb==1.3.5 black==25.11.0 pydub chardet==5.2.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index ef2007c430..79019d3291 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -48,7 +48,7 @@ langchain==0.3.27 langchain-community==0.3.29 fake-useragent==2.2.0 -chromadb==1.1.0 +chromadb==1.3.5 weaviate-client==4.17.0 opensearch-py==2.8.0 diff --git a/pyproject.toml b/pyproject.toml index ffa7d8d453..59297db60a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "langchain-community==0.3.29", "fake-useragent==2.2.0", - "chromadb==1.0.20", + "chromadb==1.3.5", "opensearch-py==2.8.0", "PyMySQL==1.1.1", "boto3==1.41.5", From dcdbafa5ae378a2e7764b4b111a34fe08d793678 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:03:42 +0100 Subject: [PATCH 20/71] chore: bump pymilvus (#19773) * Update pymilvus version to 2.6.5 * Update pyproject.toml --- backend/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 79019d3291..c9ccd9c28f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -115,7 +115,7 @@ pgvector==0.4.1 PyMySQL==1.1.1 boto3==1.41.5 -pymilvus==2.6.4 +pymilvus==2.6.5 qdrant-client==1.16.1 playwright==1.56.0 # Caution: version must match docker-compose.playwright.yaml elasticsearch==9.1.0 diff --git a/pyproject.toml b/pyproject.toml index 59297db60a..3cae025265 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,7 @@ all = [ "qdrant-client==1.16.1", "weaviate-client==4.17.0", - "pymilvus==2.6.4", + "pymilvus==2.6.5", "pinecone==6.0.2", "oracledb==3.2.0", "colbert-ai==0.2.21", From 7e6b8a9a7135daec7e7c5fb6f347e0b792384183 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Dec 2025 08:06:42 -0500 Subject: [PATCH 21/71] refac/fix: docling auth --- backend/open_webui/retrieval/loaders/main.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 1346cd065c..969ced62e0 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -144,19 +144,17 @@ class DoclingLoader: with open(self.file_path, "rb") as f: headers = {} if self.api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - - files = { - "files": ( - self.file_path, - f, - self.mime_type or "application/octet-stream", - ) - } + headers["X-Api-Key"] = f"Bearer {self.api_key}" r = requests.post( f"{self.url}/v1/convert/file", - files=files, + files={ + "files": ( + self.file_path, + f, + self.mime_type or "application/octet-stream", + ) + }, data={ "image_export_mode": "placeholder", **self.params, From 0617ede0eb1b0c55c2156c827ae3a82efd0e2202 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:27:45 +0100 Subject: [PATCH 22/71] Upgrade Playwright image and command to v1.56.0 (#19774) --- docker-compose.playwright.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.playwright.yaml b/docker-compose.playwright.yaml index fa2b49ff9a..4567a0ef62 100644 --- a/docker-compose.playwright.yaml +++ b/docker-compose.playwright.yaml @@ -1,8 +1,8 @@ services: playwright: - image: mcr.microsoft.com/playwright:v1.49.1-noble # Version must match requirements.txt + image: mcr.microsoft.com/playwright:v1.56.0-noble # Version must match requirements.txt container_name: playwright - command: npx -y playwright@1.49.1 run-server --port 3000 --host 0.0.0.0 + command: npx -y playwright@1.56.0 run-server --port 3000 --host 0.0.0.0 open-webui: environment: From 492c8bac09d006ffc45d67a03caabe15da2cc147 Mon Sep 17 00:00:00 2001 From: bitsofinfo Date: Sat, 6 Dec 2025 08:45:34 -0700 Subject: [PATCH 23/71] feat: new OAUTH_AUDIENCE config (#19768) * feat: new config AUTH0_AUDIENCE * feat: OAUTH_AUDIENCE config --- backend/open_webui/config.py | 5 +++++ backend/open_webui/utils/oauth.py | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 41e88df5d2..6d60ab89f5 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -629,6 +629,11 @@ OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = ( == "true" ) +OAUTH_AUDIENCE = PersistentConfig( + "OAUTH_AUDIENCE", + "oauth.audience", + os.environ.get("OAUTH_AUDIENCE", ""), +) def load_oauth_providers(): OAUTH_PROVIDERS.clear() diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 61c98ca744..1ef5268bae 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -55,6 +55,7 @@ from open_webui.config import ( OAUTH_ALLOWED_DOMAINS, OAUTH_UPDATE_PICTURE_ON_LOGIN, OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID, + OAUTH_AUDIENCE, WEBHOOK_URL, JWT_EXPIRES_IN, AppConfig, @@ -126,6 +127,7 @@ auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS auth_manager_config.WEBHOOK_URL = WEBHOOK_URL auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN +auth_manager_config.OAUTH_AUDIENCE = OAUTH_AUDIENCE FERNET = None @@ -1270,7 +1272,12 @@ class OAuthManager: client = self.get_client(provider) if client is None: raise HTTPException(404) - return await client.authorize_redirect(request, redirect_uri) + + kwargs = {} + if (auth_manager_config.OAUTH_AUDIENCE): + kwargs["audience"] = auth_manager_config.OAUTH_AUDIENCE + + return await client.authorize_redirect(request, redirect_uri, **kwargs) async def handle_callback(self, request, provider, response): if provider not in OAUTH_PROVIDERS: From aa9c0389c36d4f292e732b9c1cfb7bc87abc3a4d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 7 Dec 2025 18:06:06 -0500 Subject: [PATCH 24/71] refac --- src/routes/(app)/workspace/models/create/+page.svelte | 2 +- src/routes/(app)/workspace/prompts/create/+page.svelte | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/(app)/workspace/models/create/+page.svelte b/src/routes/(app)/workspace/models/create/+page.svelte index 31bdd96c38..927cca6355 100644 --- a/src/routes/(app)/workspace/models/create/+page.svelte +++ b/src/routes/(app)/workspace/models/create/+page.svelte @@ -64,7 +64,7 @@ onMount(async () => { window.addEventListener('message', async (event) => { if ( - !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:5173'].includes( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( event.origin ) ) { diff --git a/src/routes/(app)/workspace/prompts/create/+page.svelte b/src/routes/(app)/workspace/prompts/create/+page.svelte index 2fa25de6bd..f4c9a7c0d4 100644 --- a/src/routes/(app)/workspace/prompts/create/+page.svelte +++ b/src/routes/(app)/workspace/prompts/create/+page.svelte @@ -34,8 +34,9 @@ onMount(async () => { window.addEventListener('message', async (event) => { + console.log(event); if ( - !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:5173'].includes( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( event.origin ) ) From 4d4ed743ae4e2d79c41d3071d4745b21b752a80c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 7 Dec 2025 18:13:28 -0500 Subject: [PATCH 25/71] fix: styling --- src/lib/components/workspace/Models/ModelEditor.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 906bed422f..a05896f8d8 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -521,15 +521,15 @@
-
+
+ + - - {getDisplayTitle(formattedTitle(decodeString(sourceIds[token.ids[0] - 1])))} - +{(token?.ids ?? []).length - 1} - - - - -
- {#each token.ids as sourceId} -
- -
- {/each} -
-
- +
+ {#each token.ids as sourceId} +
+ +
+ {/each} +
+
+ + {/if} +{:else} + {token.raw} {/if} From ce945a93342b1064306a90712bf364d8496ae699 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 7 Dec 2025 23:56:37 -0500 Subject: [PATCH 27/71] refac --- .../chat/Messages/ResponseMessage.svelte | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 77f1848ee6..801acfbdfc 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -1460,37 +1460,35 @@ {/if} {/if} - {#if isLastMessage} - {#each model?.actions ?? [] as action} - - - - {/each} - {/if} + {#each model?.actions ?? [] as action} + + + + {/each} {/if} {/if} {/if} From 8cea0cf746da927fea358cf95f2813a53c66e1b4 Mon Sep 17 00:00:00 2001 From: _00_ <131402327+rgaricano@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:09:53 +0100 Subject: [PATCH 28/71] FIX: Pipeline save settings - Handle undefined valves property (#19791) ### FIX: Pipeline save settings - Handle undefined valves property When a Pipeline valve have a null value the settings isn't saved. The error occurs because the code tries to call `.split()` on a `null` value when saving pipeline valves. This happens when you set a valve to "None" (null) and then click save. This PR Fix this issue. --- src/lib/components/admin/Settings/Pipelines.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/admin/Settings/Pipelines.svelte b/src/lib/components/admin/Settings/Pipelines.svelte index 18446da7dd..81ecfe2218 100644 --- a/src/lib/components/admin/Settings/Pipelines.svelte +++ b/src/lib/components/admin/Settings/Pipelines.svelte @@ -47,7 +47,7 @@ if (pipeline && (pipeline?.valves ?? false)) { for (const property in valves_spec.properties) { if (valves_spec.properties[property]?.type === 'array') { - valves[property] = valves[property].split(',').map((v) => v.trim()); + valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim()); } } From bcd50ed8f1b7387fd700538ae0d74fc72f3c53d0 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 8 Dec 2025 11:30:38 -0500 Subject: [PATCH 29/71] refac --- backend/open_webui/utils/middleware.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 140d2bc85d..d397471dd9 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -716,17 +716,18 @@ async def chat_web_search_handler( return form_data -def get_last_images(message_list): +def get_images_from_messages(message_list): images = [] + for message in reversed(message_list): - images_flag = False + + message_images = [] for file in message.get("files", []): if file.get("type") == "image": - images.append(file.get("url")) - images_flag = True + message_images.append(file.get("url")) - if images_flag: - break + if message_images: + images.append(message_images) return images @@ -780,7 +781,16 @@ async def chat_image_generation_handler( user_message = get_last_user_message(message_list) prompt = user_message - input_images = get_last_images(message_list) + message_images = get_images_from_messages(message_list) + + # Limit to first 2 sets of images + # We may want to change this in the future to allow more images + input_images = [] + for idx, images in enumerate(message_images): + if idx >= 2: + break + for image in images: + input_images.append(image) system_message_content = "" From b02397e460ec56b4b74146508eeab2a3ba13950e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 8 Dec 2025 11:49:27 -0500 Subject: [PATCH 30/71] feat: WEB_LOADER_TIMEOUT --- backend/open_webui/config.py | 7 +++++++ backend/open_webui/main.py | 2 ++ backend/open_webui/retrieval/web/utils.py | 15 +++++++++++++++ backend/open_webui/routers/retrieval.py | 5 +++++ .../components/admin/Settings/WebSearch.svelte | 13 +++++++++++++ 5 files changed, 42 insertions(+) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 6d60ab89f5..983db4e04b 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -635,6 +635,7 @@ OAUTH_AUDIENCE = PersistentConfig( os.environ.get("OAUTH_AUDIENCE", ""), ) + def load_oauth_providers(): OAUTH_PROVIDERS.clear() if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value: @@ -2999,6 +3000,12 @@ WEB_LOADER_CONCURRENT_REQUESTS = PersistentConfig( int(os.getenv("WEB_LOADER_CONCURRENT_REQUESTS", "10")), ) +WEB_LOADER_TIMEOUT = PersistentConfig( + "WEB_LOADER_TIMEOUT", + "rag.web.loader.timeout", + os.getenv("WEB_LOADER_TIMEOUT", ""), +) + ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig( "ENABLE_WEB_LOADER_SSL_VERIFICATION", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index dc4468e8e7..5609289166 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -208,6 +208,7 @@ from open_webui.config import ( FIRECRAWL_API_KEY, WEB_LOADER_ENGINE, WEB_LOADER_CONCURRENT_REQUESTS, + WEB_LOADER_TIMEOUT, WHISPER_MODEL, WHISPER_VAD_FILTER, WHISPER_LANGUAGE, @@ -922,6 +923,7 @@ app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS +app.state.config.WEB_LOADER_TIMEOUT = WEB_LOADER_TIMEOUT app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ( diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index bdbde0b3a9..ec0a8d8ed7 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -33,6 +33,7 @@ from open_webui.config import ( PLAYWRIGHT_WS_URL, PLAYWRIGHT_TIMEOUT, WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, FIRECRAWL_API_BASE_URL, FIRECRAWL_API_KEY, TAVILY_API_KEY, @@ -674,6 +675,20 @@ def get_web_loader( if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web": WebLoaderClass = SafeWebBaseLoader + + request_kwargs = {} + if WEB_LOADER_TIMEOUT.value: + try: + timeout_value = float(WEB_LOADER_TIMEOUT.value) + except ValueError: + timeout_value = None + + if timeout_value: + request_kwargs["timeout"] = timeout_value + + if request_kwargs: + web_loader_args["requests_kwargs"] = request_kwargs + if WEB_LOADER_ENGINE.value == "playwright": WebLoaderClass = SafePlaywrightURLLoader web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index cc2457eba7..08ffde1733 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -536,6 +536,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID, "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK, "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE, + "WEB_LOADER_TIMEOUT": request.app.state.config.WEB_LOADER_TIMEOUT, "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL, "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT, @@ -594,6 +595,7 @@ class WebConfig(BaseModel): SOUGOU_API_SID: Optional[str] = None SOUGOU_API_SK: Optional[str] = None WEB_LOADER_ENGINE: Optional[str] = None + WEB_LOADER_TIMEOUT: Optional[str] = None ENABLE_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None PLAYWRIGHT_WS_URL: Optional[str] = None PLAYWRIGHT_TIMEOUT: Optional[int] = None @@ -1071,6 +1073,8 @@ async def update_rag_config( # Web loader settings request.app.state.config.WEB_LOADER_ENGINE = form_data.web.WEB_LOADER_ENGINE + request.app.state.config.WEB_LOADER_TIMEOUT = form_data.web.WEB_LOADER_TIMEOUT + request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ( form_data.web.ENABLE_WEB_LOADER_SSL_VERIFICATION ) @@ -1206,6 +1210,7 @@ async def update_rag_config( "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID, "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK, "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE, + "WEB_LOADER_TIMEOUT": request.app.state.config.WEB_LOADER_TIMEOUT, "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL, "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT, diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 17191ac216..f26a9f85b8 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -767,6 +767,19 @@
{#if webConfig.WEB_LOADER_ENGINE === '' || webConfig.WEB_LOADER_ENGINE === 'safe_web'} +
+
+ {$i18n.t('Timeout')} +
+
+ +
+
+
{$i18n.t('Verify SSL Certificate')} From ba158d378f18706cc85231504ede1610509cb7c4 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 8 Dec 2025 11:59:45 -0500 Subject: [PATCH 31/71] feat: REDIS_SOCKET_CONNECT_TIMEOUT Co-Authored-By: Jan Kessler --- backend/open_webui/env.py | 16 +++++++++++++++- backend/open_webui/utils/redis.py | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index e3c50ea8d1..7d6ecfada0 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -395,6 +395,13 @@ try: except ValueError: REDIS_SENTINEL_MAX_RETRY_COUNT = 2 + +REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get("REDIS_SOCKET_CONNECT_TIMEOUT", "") +try: + REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT) +except ValueError: + REDIS_SOCKET_CONNECT_TIMEOUT = None + #################################### # UVICORN WORKERS #################################### @@ -620,9 +627,16 @@ ENABLE_WEBSOCKET_SUPPORT = ( WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "") WEBSOCKET_REDIS_OPTIONS = os.environ.get("WEBSOCKET_REDIS_OPTIONS", "") + + if WEBSOCKET_REDIS_OPTIONS == "": log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None") - WEBSOCKET_REDIS_OPTIONS = None + if REDIS_SOCKET_CONNECT_TIMEOUT: + WEBSOCKET_REDIS_OPTIONS = { + "socket_connect_timeout": REDIS_SOCKET_CONNECT_TIMEOUT + } + else: + WEBSOCKET_REDIS_OPTIONS = None else: try: WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS) diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index cc29ce6683..da6df2a7f9 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -7,6 +7,7 @@ import redis from open_webui.env import ( REDIS_CLUSTER, + REDIS_SOCKET_CONNECT_TIMEOUT, REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_MAX_RETRY_COUNT, REDIS_SENTINEL_PORT, @@ -162,6 +163,7 @@ def get_redis_connection( username=redis_config["username"], password=redis_config["password"], decode_responses=decode_responses, + socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT, ) connection = SentinelRedisProxy( sentinel, @@ -188,6 +190,7 @@ def get_redis_connection( username=redis_config["username"], password=redis_config["password"], decode_responses=decode_responses, + socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT, ) connection = SentinelRedisProxy( sentinel, From f5fbbaf060e5a8e99aac3a9fcd4ae0c29483989a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 8 Dec 2025 12:18:25 -0500 Subject: [PATCH 32/71] refac: redis config log Co-Authored-By: Jan Kessler --- backend/open_webui/env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 7d6ecfada0..d49a79b3b1 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -630,12 +630,12 @@ WEBSOCKET_REDIS_OPTIONS = os.environ.get("WEBSOCKET_REDIS_OPTIONS", "") if WEBSOCKET_REDIS_OPTIONS == "": - log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None") if REDIS_SOCKET_CONNECT_TIMEOUT: WEBSOCKET_REDIS_OPTIONS = { "socket_connect_timeout": REDIS_SOCKET_CONNECT_TIMEOUT } else: + log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None") WEBSOCKET_REDIS_OPTIONS = None else: try: From 2b1a29d44bde9fbc20ff9f0a5ded1ce8ded9d90d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 8 Dec 2025 12:45:52 -0500 Subject: [PATCH 33/71] enh: display user groups in user preview --- backend/open_webui/routers/users.py | 4 +++- .../channel/Messages/Message/UserStatus.svelte | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 3c1bbb72a8..dd2077206a 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -391,6 +391,7 @@ async def update_user_info_by_session_user( class UserActiveResponse(UserStatus): name: str profile_image_url: Optional[str] = None + groups: Optional[list] = [] is_active: bool model_config = ConfigDict(extra="allow") @@ -412,11 +413,12 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): ) user = Users.get_user_by_id(user_id) - if user: + groups = Groups.get_groups_by_member_id(user_id) return UserActiveResponse( **{ **user.model_dump(), + "groups": [{"id": group.id, "name": group.name} for group in groups], "is_active": Users.is_user_active(user_id), } ) diff --git a/src/lib/components/channel/Messages/Message/UserStatus.svelte b/src/lib/components/channel/Messages/Message/UserStatus.svelte index 222f3441b5..cb40898898 100644 --- a/src/lib/components/channel/Messages/Message/UserStatus.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatus.svelte @@ -102,6 +102,18 @@
{/if} + {#if (user?.groups ?? []).length > 0} +
+ {#each user.groups as group} +
+ {group.name} +
+ {/each} +
+ {/if} + {#if $_user?.id !== user.id}
From 44e41806f20fc483b499431844f280cec3f0cfa2 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:28:21 +0100 Subject: [PATCH 34/71] chore: dep bump across many dependencies (#19850) * Update pyproject.toml (#101) * Update pyproject.toml * Update requirements.txt * Update requirements-min.txt * Upgrade Playwright version to 1.57.0 * Update langchain-community version to 0.3.29 * Update requirements.txt * Update requirements-min.txt --- backend/requirements-min.txt | 16 +++--- backend/requirements.txt | 94 +++++++++++++++++----------------- docker-compose.playwright.yaml | 4 +- pyproject.toml | 75 ++++++++++++++------------- 4 files changed, 95 insertions(+), 94 deletions(-) diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt index 714d44c1bd..bcf154279e 100644 --- a/backend/requirements-min.txt +++ b/backend/requirements-min.txt @@ -1,7 +1,7 @@ # Minimal requirements for backend to run # WIP: use this as a reference to build a minimal docker image -fastapi==0.123.0 +fastapi==0.124.0 uvicorn[standard]==0.37.0 pydantic==2.12.5 python-multipart==0.0.20 @@ -16,7 +16,7 @@ PyJWT[crypto]==2.10.1 authlib==1.6.5 requests==2.32.5 -aiohttp==3.12.15 +aiohttp==3.13.2 async-timeout aiocache aiofiles @@ -24,21 +24,21 @@ starlette-compress==1.6.1 httpx[socks,http2,zstd,cli,brotli]==0.28.1 starsessions[redis]==2.2.1 -sqlalchemy==2.0.38 +sqlalchemy==2.0.44 alembic==1.17.2 peewee==3.18.3 peewee-migrate==1.14.3 -pycrdt==0.12.25 +pycrdt==0.12.44 redis -APScheduler==3.10.4 -RestrictedPython==8.0 +APScheduler==3.11.1 +RestrictedPython==8.1 loguru==0.7.3 asgiref==3.11.0 -mcp==1.22.0 +mcp==1.23.1 openai langchain==0.3.27 @@ -46,6 +46,6 @@ langchain-community==0.3.29 fake-useragent==2.2.0 chromadb==1.3.5 -black==25.11.0 +black==25.12.0 pydub chardet==5.2.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index c9ccd9c28f..558b6ecc46 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.123.0 +fastapi==0.124.0 uvicorn[standard]==0.37.0 pydantic==2.12.5 python-multipart==0.0.20 @@ -13,7 +13,7 @@ PyJWT[crypto]==2.10.1 authlib==1.6.5 requests==2.32.5 -aiohttp==3.12.15 +aiohttp==3.13.2 async-timeout aiocache aiofiles @@ -21,27 +21,27 @@ starlette-compress==1.6.1 httpx[socks,http2,zstd,cli,brotli]==0.28.1 starsessions[redis]==2.2.1 -sqlalchemy==2.0.38 +sqlalchemy==2.0.44 alembic==1.17.2 peewee==3.18.3 peewee-migrate==1.14.3 -pycrdt==0.12.25 +pycrdt==0.12.44 redis -APScheduler==3.10.4 -RestrictedPython==8.0 +APScheduler==3.11.1 +RestrictedPython==8.1 loguru==0.7.3 asgiref==3.11.0 # AI libraries tiktoken -mcp==1.22.0 +mcp==1.23.3 openai anthropic -google-genai==1.52.0 +google-genai==1.54.0 google-generativeai==0.8.5 langchain==0.3.27 @@ -49,8 +49,8 @@ langchain-community==0.3.29 fake-useragent==2.2.0 chromadb==1.3.5 -weaviate-client==4.17.0 -opensearch-py==2.8.0 +weaviate-client==4.18.3 +opensearch-py==3.1.0 transformers==4.57.3 sentence-transformers==5.1.2 @@ -60,43 +60,43 @@ einops==0.8.1 ftfy==6.3.1 chardet==5.2.0 -pypdf==6.4.0 -fpdf2==2.8.2 -pymdown-extensions==10.17.2 -docx2txt==0.8 +pypdf==6.4.1 +fpdf2==2.8.5 +pymdown-extensions==10.18 +docx2txt==0.9 python-pptx==1.0.2 unstructured==0.18.21 msoffcrypto-tool==5.4.2 -nltk==3.9.1 +nltk==3.9.2 Markdown==3.10 pypandoc==1.16.2 -pandas==2.2.3 +pandas==2.3.3 openpyxl==3.1.5 pyxlsb==1.0.10 -xlrd==2.0.1 +xlrd==2.0.2 validators==0.35.0 psutil sentencepiece soundfile==0.13.1 -pillow==11.3.0 -opencv-python-headless==4.11.0.86 +pillow==12.0.0 +opencv-python-headless==4.12.0.88 rapidocr-onnxruntime==1.4.4 rank-bm25==0.2.2 -onnxruntime==1.20.1 -faster-whisper==1.1.1 +onnxruntime==1.23.2 +faster-whisper==1.2.1 -black==25.11.0 -youtube-transcript-api==1.2.2 +black==25.12.0 +youtube-transcript-api==1.2.3 pytube==15.0.0 pydub -ddgs==9.9.2 +ddgs==9.9.3 azure-ai-documentintelligence==1.0.2 -azure-identity==1.25.0 -azure-storage-blob==12.24.1 +azure-identity==1.25.1 +azure-storage-blob==12.27.1 azure-search-documents==11.6.0 ## Google Drive @@ -105,26 +105,26 @@ google-auth-httplib2 google-auth-oauthlib googleapis-common-protos==1.72.0 -google-cloud-storage==2.19.0 +google-cloud-storage==3.7.0 ## Databases pymongo -psycopg2-binary==2.9.10 -pgvector==0.4.1 +psycopg2-binary==2.9.11 +pgvector==0.4.2 -PyMySQL==1.1.1 -boto3==1.41.5 +PyMySQL==1.1.2 +boto3==1.42.5 pymilvus==2.6.5 qdrant-client==1.16.1 -playwright==1.56.0 # Caution: version must match docker-compose.playwright.yaml -elasticsearch==9.1.0 +playwright==1.57.0 # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary +elasticsearch==9.2.0 pinecone==6.0.2 -oracledb==3.2.0 +oracledb==3.4.1 av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720 -colbert-ai==0.2.21 +colbert-ai==0.2.22 ## Tests @@ -136,17 +136,17 @@ pytest-docker~=3.2.5 ldap3==2.9.1 ## Firecrawl -firecrawl-py==4.10.0 +firecrawl-py==4.10.4 ## Trace -opentelemetry-api==1.38.0 -opentelemetry-sdk==1.38.0 -opentelemetry-exporter-otlp==1.38.0 -opentelemetry-instrumentation==0.59b0 -opentelemetry-instrumentation-fastapi==0.59b0 -opentelemetry-instrumentation-sqlalchemy==0.59b0 -opentelemetry-instrumentation-redis==0.59b0 -opentelemetry-instrumentation-requests==0.59b0 -opentelemetry-instrumentation-logging==0.59b0 -opentelemetry-instrumentation-httpx==0.59b0 -opentelemetry-instrumentation-aiohttp-client==0.59b0 +opentelemetry-api==1.39.0 +opentelemetry-sdk==1.39.0 +opentelemetry-exporter-otlp==1.39.0 +opentelemetry-instrumentation==0.60b0 +opentelemetry-instrumentation-fastapi==0.60b0 +opentelemetry-instrumentation-sqlalchemy==0.60b0 +opentelemetry-instrumentation-redis==0.60b0 +opentelemetry-instrumentation-requests==0.60b0 +opentelemetry-instrumentation-logging==0.60b0 +opentelemetry-instrumentation-httpx==0.60b0 +opentelemetry-instrumentation-aiohttp-client==0.60b0 diff --git a/docker-compose.playwright.yaml b/docker-compose.playwright.yaml index 4567a0ef62..e00a28df58 100644 --- a/docker-compose.playwright.yaml +++ b/docker-compose.playwright.yaml @@ -1,8 +1,8 @@ services: playwright: - image: mcr.microsoft.com/playwright:v1.56.0-noble # Version must match requirements.txt + image: mcr.microsoft.com/playwright:v1.57.0-noble # Version must match requirements.txt container_name: playwright - command: npx -y playwright@1.56.0 run-server --port 3000 --host 0.0.0.0 + command: npx -y playwright@1.57.0 run-server --port 3000 --host 0.0.0.0 open-webui: environment: diff --git a/pyproject.toml b/pyproject.toml index 3cae025265..b160b67dc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] license = { file = "LICENSE" } dependencies = [ - "fastapi==0.123.0", + "fastapi==0.124.0", "uvicorn[standard]==0.37.0", "pydantic==2.12.5", "python-multipart==0.0.20", @@ -21,7 +21,7 @@ dependencies = [ "authlib==1.6.5", "requests==2.32.5", - "aiohttp==3.12.15", + "aiohttp==3.13.2", "async-timeout", "aiocache", "aiofiles", @@ -29,26 +29,26 @@ dependencies = [ "httpx[socks,http2,zstd,cli,brotli]==0.28.1", "starsessions[redis]==2.2.1", - "sqlalchemy==2.0.38", + "sqlalchemy==2.0.44", "alembic==1.17.2", "peewee==3.18.3", "peewee-migrate==1.14.3", - "pycrdt==0.12.25", + "pycrdt==0.12.44", "redis", - "APScheduler==3.10.4", - "RestrictedPython==8.0", + "APScheduler==3.11.1", + "RestrictedPython==8.1", "loguru==0.7.3", "asgiref==3.11.0", "tiktoken", - "mcp==1.22.0", + "mcp==1.23.3", "openai", "anthropic", - "google-genai==1.52.0", + "google-genai==1.54.0", "google-generativeai==0.8.5", "langchain==0.3.27", @@ -56,62 +56,62 @@ dependencies = [ "fake-useragent==2.2.0", "chromadb==1.3.5", - "opensearch-py==2.8.0", - "PyMySQL==1.1.1", - "boto3==1.41.5", + "opensearch-py==3.1.0", + "PyMySQL==1.1.2", + "boto3==1.42.5", "transformers==4.57.3", "sentence-transformers==5.1.2", "accelerate", - "pyarrow==20.0.0", + "pyarrow==20.0.0", # fix: pin pyarrow version to 20 for rpi compatibility #15897 "einops==0.8.1", "ftfy==6.3.1", "chardet==5.2.0", - "pypdf==6.4.0", - "fpdf2==2.8.2", - "pymdown-extensions==10.17.2", - "docx2txt==0.8", + "pypdf==6.4.1", + "fpdf2==2.8.5", + "pymdown-extensions==10.18", + "docx2txt==0.9", "python-pptx==1.0.2", "unstructured==0.18.21", "msoffcrypto-tool==5.4.2", - "nltk==3.9.1", + "nltk==3.9.2", "Markdown==3.10", "pypandoc==1.16.2", - "pandas==2.2.3", + "pandas==2.3.3", "openpyxl==3.1.5", "pyxlsb==1.0.10", - "xlrd==2.0.1", + "xlrd==2.0.2", "validators==0.35.0", "psutil", "sentencepiece", "soundfile==0.13.1", "azure-ai-documentintelligence==1.0.2", - "pillow==11.3.0", - "opencv-python-headless==4.11.0.86", + "pillow==12.0.0", + "opencv-python-headless==4.12.0.88", "rapidocr-onnxruntime==1.4.4", "rank-bm25==0.2.2", - "onnxruntime==1.20.1", - "faster-whisper==1.1.1", + "onnxruntime==1.23.2", + "faster-whisper==1.2.1", - "black==25.11.0", - "youtube-transcript-api==1.2.2", + "black==25.12.0", + "youtube-transcript-api==1.2.3", "pytube==15.0.0", "pydub", - "ddgs==9.9.2", + "ddgs==9.9.3", "google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib", "googleapis-common-protos==1.72.0", - "google-cloud-storage==2.19.0", + "google-cloud-storage==3.7.0", - "azure-identity==1.25.0", - "azure-storage-blob==12.24.1", + "azure-identity==1.25.1", + "azure-storage-blob==12.27.1", "ldap3==2.9.1", ] @@ -130,8 +130,8 @@ classifiers = [ [project.optional-dependencies] postgres = [ - "psycopg2-binary==2.9.10", - "pgvector==0.4.1", + "psycopg2-binary==2.9.11", + "pgvector==0.4.2", ] all = [ @@ -143,17 +143,18 @@ all = [ "docker~=7.1.0", "pytest~=8.3.2", "pytest-docker~=3.2.5", - "playwright==1.56.0", - "elasticsearch==9.1.0", + "playwright==1.57.0", # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary + "elasticsearch==9.2.0", "qdrant-client==1.16.1", - "weaviate-client==4.17.0", + "pymilvus==2.6.4", + "weaviate-client==4.18.3", "pymilvus==2.6.5", "pinecone==6.0.2", - "oracledb==3.2.0", - "colbert-ai==0.2.21", + "oracledb==3.4.1", + "colbert-ai==0.2.22", - "firecrawl-py==4.10.0", + "firecrawl-py==4.10.4", "azure-search-documents==11.6.0", ] From c24b1207a07fbe56e110dadcd98a8a9ca872b042 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Wed, 10 Dec 2025 04:29:27 +0800 Subject: [PATCH 35/71] fix: fixed missing text in the explanation feature (#19829) --- backend/open_webui/utils/misc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 5e3f3c4834..21943caff8 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -624,14 +624,17 @@ def stream_chunks_handler(stream: aiohttp.StreamReader): yield line else: yield b"data: {}" + yield b"\n" else: # Normal mode: check if line exceeds limit if len(line) > max_buffer_size: skip_mode = True yield b"data: {}" + yield b"\n" log.info(f"Skip mode triggered, line size: {len(line)}") else: yield line + yield b"\n" # Save the last incomplete fragment buffer = lines[-1] @@ -646,5 +649,6 @@ def stream_chunks_handler(stream: aiohttp.StreamReader): # Process remaining buffer data if buffer and not skip_mode: yield buffer + yield b"\n" return yield_safe_stream_chunks() From 1ea555a5acda29eea2193ec0616c7064f0a4e7f2 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Wed, 10 Dec 2025 04:29:43 +0800 Subject: [PATCH 36/71] i18n: improve Chinese translation (#19830) --- src/lib/i18n/locales/zh-CN/translation.json | 1 + src/lib/i18n/locales/zh-TW/translation.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index c8d7b350e8..3b54fa6c69 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -1624,6 +1624,7 @@ "Tika": "Tika", "Tika Server URL required.": "请输入 Tika 服务器接口地址", "Tiktoken": "Tiktoken", + "Timeout": "超时时间", "Title": "标题", "Title Auto-Generation": "自动生成标题", "Title cannot be an empty string.": "标题不能为空", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index 6e610e3fa1..38cd055dac 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -1624,6 +1624,7 @@ "Tika": "Tika", "Tika Server URL required.": "需要提供 Tika 伺服器 URL。", "Tiktoken": "Tiktoken", + "Timeout": "逾時時間", "Title": "標題", "Title Auto-Generation": "自動產生標題", "Title cannot be an empty string.": "標題不能是空字串。", From 9b24cddef6c4862bd899eb8d6332cafff54e871d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 16:45:08 -0500 Subject: [PATCH 37/71] enh/refac: notes --- backend/open_webui/models/notes.py | 205 ++++-- backend/open_webui/routers/notes.py | 102 ++- src/lib/apis/notes/index.ts | 56 +- .../components/common/DropdownOptions.svelte | 54 ++ src/lib/components/notes/Notes.svelte | 588 +++++++++++------- 5 files changed, 688 insertions(+), 317 deletions(-) create mode 100644 src/lib/components/common/DropdownOptions.svelte diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index af75fab598..0942ad0140 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -7,12 +7,15 @@ from functools import lru_cache from open_webui.internal.db import Base, get_db from open_webui.models.groups import Groups from open_webui.utils.access_control import has_access -from open_webui.models.users import Users, UserResponse +from open_webui.models.users import User, UserModel, Users, UserResponse from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON -from sqlalchemy import or_, func, select, and_, text +from sqlalchemy.dialects.postgresql import JSONB + + +from sqlalchemy import or_, func, select, and_, text, cast, or_, and_, func from sqlalchemy.sql import exists #################### @@ -75,7 +78,63 @@ class NoteUserResponse(NoteModel): user: Optional[UserResponse] = None +class NoteItemResponse(BaseModel): + id: str + title: str + data: Optional[dict] + updated_at: int + created_at: int + user: Optional[UserResponse] = None + + +class NoteListResponse(BaseModel): + items: list[NoteUserResponse] + total: int + + class NoteTable: + def _has_permission(self, db, query, filter: dict, permission: str = "read"): + group_ids = filter.get("group_ids", []) + user_id = filter.get("user_id") + + dialect_name = db.bind.dialect.name + + # Public access + conditions = [] + if group_ids or user_id: + conditions.extend( + [ + Note.access_control.is_(None), + cast(Note.access_control, String) == "null", + ] + ) + + # User-level permission + if user_id: + conditions.append(Note.user_id == user_id) + + # Group-level permission + if group_ids: + group_conditions = [] + for gid in group_ids: + if dialect_name == "sqlite": + group_conditions.append( + Note.access_control[permission]["group_ids"].contains([gid]) + ) + elif dialect_name == "postgresql": + group_conditions.append( + cast( + Note.access_control[permission]["group_ids"], + JSONB, + ).contains([gid]) + ) + conditions.append(or_(*group_conditions)) + + if conditions: + query = query.filter(or_(*conditions)) + + return query + def insert_new_note( self, form_data: NoteForm, @@ -110,15 +169,103 @@ class NoteTable: notes = query.all() return [NoteModel.model_validate(note) for note in notes] + def search_notes( + self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30 + ) -> NoteListResponse: + with get_db() as db: + query = db.query(Note, User).outerjoin(User, User.id == Note.user_id) + if filter: + query_key = filter.get("query") + if query_key: + query = query.filter( + or_( + Note.title.ilike(f"%{query_key}%"), + Note.data["content"]["md"].ilike(f"%{query_key}%"), + ) + ) + + view_option = filter.get("view_option") + if view_option == "created": + query = query.filter(Note.user_id == user_id) + elif view_option == "shared": + query = query.filter(Note.user_id != user_id) + + # Apply access control filtering + query = self._has_permission( + db, + query, + filter, + permission="write", + ) + + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by == "name": + if direction == "asc": + query = query.order_by(Note.title.asc()) + else: + query = query.order_by(Note.title.desc()) + elif order_by == "created_at": + if direction == "asc": + query = query.order_by(Note.created_at.asc()) + else: + query = query.order_by(Note.created_at.desc()) + elif order_by == "updated_at": + if direction == "asc": + query = query.order_by(Note.updated_at.asc()) + else: + query = query.order_by(Note.updated_at.desc()) + else: + query = query.order_by(Note.updated_at.desc()) + + else: + query = query.order_by(Note.updated_at.desc()) + + for key, value in filter.items(): + query = query.filter(getattr(Note, key).ilike(f"%{value}%")) + + # Count BEFORE pagination + total = query.count() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + items = query.all() + + notes = [] + for note, user in items: + notes.append( + NoteUserResponse( + **NoteModel.model_validate(note).model_dump(), + user=( + UserResponse(**UserModel.model_validate(user).model_dump()) + if user + else None + ), + ) + ) + + return NoteListResponse(items=notes, total=total) + def get_notes_by_user_id( self, user_id: str, + permission: str = "read", skip: Optional[int] = None, limit: Optional[int] = None, ) -> list[NoteModel]: with get_db() as db: - query = db.query(Note).filter(Note.user_id == user_id) - query = query.order_by(Note.updated_at.desc()) + user_group_ids = [ + group.id for group in Groups.get_groups_by_member_id(user_id) + ] + + query = db.query(Note).order_by(Note.updated_at.desc()) + query = self._has_permission( + db, query, {"user_id": user_id, "group_ids": user_group_ids}, permission + ) if skip is not None: query = query.offset(skip) @@ -128,56 +275,6 @@ class NoteTable: notes = query.all() return [NoteModel.model_validate(note) for note in notes] - def get_notes_by_permission( - self, - user_id: str, - permission: str = "write", - skip: Optional[int] = None, - limit: Optional[int] = None, - ) -> list[NoteModel]: - with get_db() as db: - user_groups = Groups.get_groups_by_member_id(user_id) - user_group_ids = {group.id for group in user_groups} - - # Order newest-first. We stream to keep memory usage low. - query = ( - db.query(Note) - .order_by(Note.updated_at.desc()) - .execution_options(stream_results=True) - .yield_per(256) - ) - - results: list[NoteModel] = [] - n_skipped = 0 - - for note in query: - # Fast-pass #1: owner - if note.user_id == user_id: - permitted = True - # Fast-pass #2: public/open - elif note.access_control is None: - # Technically this should mean public access for both read and write, but we'll only do read for now - # We might want to change this behavior later - permitted = permission == "read" - else: - permitted = has_access( - user_id, permission, note.access_control, user_group_ids - ) - - if not permitted: - continue - - # Apply skip AFTER permission filtering so it counts only accessible notes - if skip and n_skipped < skip: - n_skipped += 1 - continue - - results.append(NoteModel.model_validate(note)) - if limit is not None and len(results) >= limit: - break - - return results - def get_note_by_id(self, id: str) -> Optional[NoteModel]: with get_db() as db: note = db.query(Note).filter(Note.id == id).first() diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 3858c4670f..6cbbc4eaf3 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -8,11 +8,21 @@ from pydantic import BaseModel from open_webui.socket.main import sio - +from open_webui.models.groups import Groups from open_webui.models.users import Users, UserResponse -from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse +from open_webui.models.notes import ( + NoteListResponse, + Notes, + NoteModel, + NoteForm, + NoteUserResponse, +) -from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.config import ( + BYPASS_ADMIN_ACCESS_CONTROL, + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_EXPORT, +) from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS @@ -30,39 +40,17 @@ router = APIRouter() ############################ -@router.get("/", response_model=list[NoteUserResponse]) -async def get_notes(request: Request, user=Depends(get_verified_user)): - - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) - - notes = [ - NoteUserResponse( - **{ - **note.model_dump(), - "user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()), - } - ) - for note in Notes.get_notes_by_permission(user.id, "write") - ] - - return notes - - -class NoteTitleIdResponse(BaseModel): +class NoteItemResponse(BaseModel): id: str title: str + data: Optional[dict] updated_at: int created_at: int + user: Optional[UserResponse] = None -@router.get("/list", response_model=list[NoteTitleIdResponse]) -async def get_note_list( +@router.get("/", response_model=list[NoteItemResponse]) +async def get_notes( request: Request, page: Optional[int] = None, user=Depends(get_verified_user) ): if user.role != "admin" and not has_permission( @@ -80,15 +68,61 @@ async def get_note_list( skip = (page - 1) * limit notes = [ - NoteTitleIdResponse(**note.model_dump()) - for note in Notes.get_notes_by_permission( - user.id, "write", skip=skip, limit=limit + NoteUserResponse( + **{ + **note.model_dump(), + "user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()), + } ) + for note in Notes.get_notes_by_user_id(user.id, "read", skip=skip, limit=limit) ] - return notes +@router.get("/search", response_model=NoteListResponse) +async def search_notes( + request: Request, + query: Optional[str] = None, + view_option: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), +): + if user.role != "admin" and not has_permission( + user.id, "features.notes", request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + limit = None + skip = None + if page is not None: + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + if view_option: + filter["view_option"] = view_option + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: + groups = Groups.get_groups_by_member_id(user.id) + if groups: + filter["group_ids"] = [group.id for group in groups] + + filter["user_id"] = user.id + + return Notes.search_notes(user.id, filter, skip=skip, limit=limit) + + ############################ # CreateNewNote ############################ diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index 61794f6766..945b8f2261 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -91,6 +91,60 @@ export const getNotes = async (token: string = '', raw: boolean = false) => { return grouped; }; +export const searchNotes = async ( + token: string = '', + query: string | null = null, + viewOption: string | null = null, + sortKey: string | null = null, + page: number | null = null +) => { + let error = null; + const searchParams = new URLSearchParams(); + + if (query !== null) { + searchParams.append('query', query); + } + + if (viewOption !== null) { + searchParams.append('view_option', viewOption); + } + + if (sortKey !== null) { + searchParams.append('order_by', sortKey); + } + + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 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.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getNoteList = async (token: string = '', page: number | null = null) => { let error = null; const searchParams = new URLSearchParams(); @@ -99,7 +153,7 @@ export const getNoteList = async (token: string = '', page: number | null = null searchParams.append('page', `${page}`); } - const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list?${searchParams.toString()}`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/components/common/DropdownOptions.svelte b/src/lib/components/common/DropdownOptions.svelte new file mode 100644 index 0000000000..f2640672c4 --- /dev/null +++ b/src/lib/components/common/DropdownOptions.svelte @@ -0,0 +1,54 @@ + + + + +
+ {items.find((item) => item.value === value)?.label ?? placeholder} + +
+
+ + +
+ {#each items as item} + + {/each} +
+
+
diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 2b377bda6c..50c3dc1556 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -1,9 +1,7 @@ @@ -236,7 +277,7 @@ -
+
{#if loaded} -
-
+
+
+
+
+ {$i18n.t('Notes')} +
+ +
+ {total} +
+
+ +
+ +
+
+
+ +
+
@@ -277,191 +351,249 @@ {/if}
-
-
- {#if Object.keys(notes).length > 0} -
- {#each Object.keys(notes) as timeRange} -
- {$i18n.t(timeRange)} -
+
+
{ + if (e.deltaY !== 0) { + e.preventDefault(); + e.currentTarget.scrollLeft += e.deltaY; + } + }} + > +
+ +
+
-
- {#each notes[timeRange] as note, idx (note.id)} - + + {#if (items ?? []).length > 0} + {@const notes = groupNotes(items)} + +
+
+ {#each Object.keys(notes) as timeRange} +
+ {$i18n.t(timeRange)} +
+ + {#if displayOption === null} +
- {/each} -
- {/each} + {:else if displayOption === 'grid'} + + {/if} + {/each} + + {#if !allItemsLoaded} + { + if (!itemsLoading) { + loadMoreItems(); + } + }} + > +
+ +
{$i18n.t('Loading...')}
+
+
+ {/if} +
{:else}
-
-
+
+
{$i18n.t('No Notes')}
-
+
{$i18n.t('Create your first note by clicking on the plus button below.')}
{/if}
- -
-
- - - - - -
-
- - {:else}
From 307b37d5e2992653d78173d5cc221225ff00179e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 17:19:42 -0500 Subject: [PATCH 38/71] refac --- .../components/common/DropdownOptions.svelte | 3 ++ src/lib/components/notes/Notes.svelte | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/lib/components/common/DropdownOptions.svelte b/src/lib/components/common/DropdownOptions.svelte index f2640672c4..91f8607538 100644 --- a/src/lib/components/common/DropdownOptions.svelte +++ b/src/lib/components/common/DropdownOptions.svelte @@ -15,6 +15,8 @@ { value: 'top', label: $i18n.t('Top') } ]; + export let onChange: (value: string) => void = () => {}; + let open = false; @@ -44,6 +46,7 @@ on:click={() => { value = item.value; open = false; + onChange(value); }} > {item.label} diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 50c3dc1556..04f7e4a261 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -161,10 +161,9 @@ const init = async () => { reset(); await getItemsPage(); - loaded = true; }; - $: if (query !== undefined && sortKey !== undefined && viewOption !== undefined) { + $: if (loaded && query !== undefined && sortKey !== undefined && viewOption !== undefined) { init(); } @@ -251,6 +250,11 @@ }; onMount(async () => { + viewOption = localStorage?.noteViewOption ?? null; + displayOption = localStorage?.noteDisplayOption ?? null; + + loaded = true; + const dropzoneElement = document.getElementById('notes-container'); dropzoneElement?.addEventListener('dragover', onDragOver); dropzoneElement?.addEventListener('drop', onDrop); @@ -374,6 +378,13 @@ { value: 'created', label: $i18n.t('Created by you') }, { value: 'shared', label: $i18n.t('Shared with you') } ]} + onChange={(value) => { + if (value) { + localStorage.noteViewOption = value; + } else { + delete localStorage.noteViewOption; + } + }} />
@@ -386,6 +397,13 @@ { value: null, label: $i18n.t('List') }, { value: 'grid', label: $i18n.t('Grid') } ]} + onChange={() => { + if (displayOption) { + localStorage.noteDisplayOption = displayOption; + } else { + delete localStorage.noteDisplayOption; + } + }} />
@@ -394,16 +412,16 @@ {@const notes = groupNotes(items)}
-
+
{#each Object.keys(notes) as timeRange}
{$i18n.t(timeRange)}
{#if displayOption === null} -
+
{#each notes[timeRange] as note, idx (note.id)}
Date: Tue, 9 Dec 2025 17:57:15 -0500 Subject: [PATCH 39/71] enh: read only notes --- backend/open_webui/models/notes.py | 88 ++++++++++- backend/open_webui/routers/notes.py | 15 +- src/lib/apis/notes/index.ts | 5 + src/lib/components/notes/NoteEditor.svelte | 171 +++++++++++---------- src/lib/components/notes/Notes.svelte | 57 +++++-- 5 files changed, 236 insertions(+), 100 deletions(-) diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index 0942ad0140..d61094b6ff 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -96,11 +96,86 @@ class NoteTable: def _has_permission(self, db, query, filter: dict, permission: str = "read"): group_ids = filter.get("group_ids", []) user_id = filter.get("user_id") - dialect_name = db.bind.dialect.name - # Public access conditions = [] + + # Handle read_only permission separately + if permission == "read_only": + # For read_only, we want items where: + # 1. User has explicit read permission (via groups or user-level) + # 2. BUT does NOT have write permission + # 3. Public items are NOT considered read_only + + read_conditions = [] + + # Group-level read permission + if group_ids: + group_read_conditions = [] + for gid in group_ids: + if dialect_name == "sqlite": + group_read_conditions.append( + Note.access_control["read"]["group_ids"].contains([gid]) + ) + elif dialect_name == "postgresql": + group_read_conditions.append( + cast( + Note.access_control["read"]["group_ids"], + JSONB, + ).contains([gid]) + ) + + if group_read_conditions: + read_conditions.append(or_(*group_read_conditions)) + + # Combine read conditions + if read_conditions: + has_read = or_(*read_conditions) + else: + # If no read conditions, return empty result + return query.filter(False) + + # Now exclude items where user has write permission + write_exclusions = [] + + # Exclude items owned by user (they have implicit write) + if user_id: + write_exclusions.append(Note.user_id != user_id) + + # Exclude items where user has explicit write permission via groups + if group_ids: + group_write_conditions = [] + for gid in group_ids: + if dialect_name == "sqlite": + group_write_conditions.append( + Note.access_control["write"]["group_ids"].contains([gid]) + ) + elif dialect_name == "postgresql": + group_write_conditions.append( + cast( + Note.access_control["write"]["group_ids"], + JSONB, + ).contains([gid]) + ) + + if group_write_conditions: + # User should NOT have write permission + write_exclusions.append(~or_(*group_write_conditions)) + + # Exclude public items (items without access_control) + write_exclusions.append(Note.access_control.isnot(None)) + write_exclusions.append(cast(Note.access_control, String) != "null") + + # Combine: has read AND does not have write AND not public + if write_exclusions: + query = query.filter(and_(has_read, *write_exclusions)) + else: + query = query.filter(has_read) + + return query + + # Original logic for other permissions (read, write, etc.) + # Public access conditions if group_ids or user_id: conditions.extend( [ @@ -109,7 +184,7 @@ class NoteTable: ] ) - # User-level permission + # User-level permission (owner has all permissions) if user_id: conditions.append(Note.user_id == user_id) @@ -191,11 +266,16 @@ class NoteTable: query = query.filter(Note.user_id != user_id) # Apply access control filtering + if "permission" in filter: + permission = filter["permission"] + else: + permission = "write" + query = self._has_permission( db, query, filter, - permission="write", + permission=permission, ) order_by = filter.get("order_by") diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 6cbbc4eaf3..cf8eb6112f 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -84,6 +84,7 @@ async def search_notes( request: Request, query: Optional[str] = None, view_option: Optional[str] = None, + permission: Optional[str] = None, order_by: Optional[str] = None, direction: Optional[str] = None, page: Optional[int] = 1, @@ -108,6 +109,8 @@ async def search_notes( filter["query"] = query if view_option: filter["view_option"] = view_option + if permission: + filter["permission"] = permission if order_by: filter["order_by"] = order_by if direction: @@ -156,7 +159,11 @@ async def create_new_note( ############################ -@router.get("/{id}", response_model=Optional[NoteModel]) +class NoteResponse(NoteModel): + write_access: bool = False + + +@router.get("/{id}", response_model=Optional[NoteResponse]) async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_user)): if user.role != "admin" and not has_permission( user.id, "features.notes", request.app.state.config.USER_PERMISSIONS @@ -180,7 +187,11 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) - return note + write_access = has_access( + user.id, type="write", access_control=note.access_control, strict=False + ) + + return NoteResponse(**note.model_dump(), write_access=write_access) ############################ diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index 945b8f2261..55f9427e0d 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -95,6 +95,7 @@ export const searchNotes = async ( token: string = '', query: string | null = null, viewOption: string | null = null, + permission: string | null = null, sortKey: string | null = null, page: number | null = null ) => { @@ -109,6 +110,10 @@ export const searchNotes = async ( searchParams.append('view_option', viewOption); } + if (permission !== null) { + searchParams.append('permission', permission); + } + if (sortKey !== null) { searchParams.append('order_by', sortKey); } diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index f49d8bb7d0..069d717a87 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -157,6 +157,16 @@ if (res) { note = res; files = res.data.files || []; + + if (note?.write_access) { + $socket?.emit('join-note', { + note_id: id, + auth: { + token: localStorage.token + } + }); + $socket?.on('note-events', noteEventHandler); + } } else { goto('/'); return; @@ -781,13 +791,6 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, onMount(async () => { await tick(); - $socket?.emit('join-note', { - note_id: id, - auth: { - token: localStorage.token - } - }); - $socket?.on('note-events', noteEventHandler); if ($settings?.models) { selectedModelId = $settings?.models[0]; @@ -956,70 +959,72 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, {/if}
- {#if editor} -
-
- + {#if note?.write_access} + {#if editor} +
+
+ - + +
-
+ {/if} + + + + + + + + {/if} - - - - - - - - { downloadHandler(type); @@ -1071,11 +1076,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, }} >
- + {#if note?.write_access} + + {:else} +
+ {$i18n.t('Read-Only Access')} +
+ {/if} {#if editor}
@@ -1158,7 +1165,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, image={true} {files} placeholder={$i18n.t('Write something...')} - editable={versionIdx === null && !editing} + editable={versionIdx === null && !editing && note?.write_access} onSelectionUpdate={({ editor }) => { const { from, to } = editor.state.selection; const selectedText = editor.state.doc.textBetween(from, to, ' '); diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 04f7e4a261..c0d461205f 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -163,17 +163,33 @@ await getItemsPage(); }; - $: if (loaded && query !== undefined && sortKey !== undefined && viewOption !== undefined) { + $: if ( + loaded && + query !== undefined && + sortKey !== undefined && + permission !== undefined && + viewOption !== undefined + ) { init(); } const getItemsPage = async () => { itemsLoading = true; - const res = await searchNotes(localStorage.token, query, viewOption, sortKey, page).catch( - () => { - return []; - } - ); + + if (viewOption === 'created') { + permission = null; + } + + const res = await searchNotes( + localStorage.token, + query, + viewOption, + permission, + sortKey, + page + ).catch(() => { + return []; + }); if (res) { console.log(res); @@ -367,7 +383,7 @@ }} >
+ + {#if [null, 'shared'].includes(viewOption)} + + {/if}
@@ -411,17 +438,21 @@ {#if (items ?? []).length > 0} {@const notes = groupNotes(items)} -
+
- {#each Object.keys(notes) as timeRange} + {#each Object.keys(notes) as timeRange, idx}
{$i18n.t(timeRange)}
{#if displayOption === null} -
+
{#each notes[timeRange] as note, idx (note.id)}
{:else if displayOption === 'grid'}
{#each notes[timeRange] as note, idx (note.id)}
Date: Tue, 9 Dec 2025 18:03:01 -0500 Subject: [PATCH 40/71] refac --- src/routes/(app)/notes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(app)/notes/+page.svelte b/src/routes/(app)/notes/+page.svelte index b453a332d2..24dbc9bd16 100644 --- a/src/routes/(app)/notes/+page.svelte +++ b/src/routes/(app)/notes/+page.svelte @@ -110,7 +110,7 @@
-
+
From a4fe823893ca305bcb86591772acd4c4c0bafed6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 19:45:13 -0500 Subject: [PATCH 41/71] refac: styling --- src/lib/components/notes/Notes.svelte | 364 +++++++++++++------------- 1 file changed, 188 insertions(+), 176 deletions(-) diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index c0d461205f..fa1b6a0253 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -435,36 +435,164 @@
- {#if (items ?? []).length > 0} - {@const notes = groupNotes(items)} + {#if items !== null && total !== null} + {#if (items ?? []).length > 0} + {@const notes = groupNotes(items)} -
-
- {#each Object.keys(notes) as timeRange, idx} -
- {$i18n.t(timeRange)} -
- - {#if displayOption === null} +
+
+ {#each Object.keys(notes) as timeRange, idx}
- {#each notes[timeRange] as note, idx (note.id)} -
- -
-
-
- {note.title} + {$i18n.t(timeRange)} +
+ + {#if displayOption === null} +
+ {:else if displayOption === 'grid'} + - {/each} -
- {:else if displayOption === 'grid'} - - {/if} - {/each} + {/each} +
+ {/if} + {/each} - {#if !allItemsLoaded} - { - if (!itemsLoading) { - loadMoreItems(); - } - }} - > -
{ + if (!itemsLoading) { + loadMoreItems(); + } + }} > - -
{$i18n.t('Loading...')}
-
-
- {/if} -
-
- {:else} -
-
-
- {$i18n.t('No Notes')} +
+ +
{$i18n.t('Loading...')}
+
+ + {/if}
+
+ {:else} +
+
+
+ {$i18n.t('No Notes')} +
-
- {$i18n.t('Create your first note by clicking on the plus button below.')} +
+ {$i18n.t('Create your first note by clicking on the plus button below.')} +
+ {/if} + {:else} +
+
{/if}
{:else}
- +
{/if}
From 65d4b22c7cfcd07ce803ee3e229ff199d3f896d6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 19:47:38 -0500 Subject: [PATCH 42/71] refac --- src/lib/components/notes/Notes.svelte | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index fa1b6a0253..9204435ed3 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -461,14 +461,24 @@
-
- {note.title} -
+ +
+ {note.title} +
+
-
- {dayjs(note.updated_at / 1000000).fromNow()} -
+ +
+ {dayjs(note.updated_at / 1000000).fromNow()} +
+
Date: Tue, 9 Dec 2025 20:49:46 -0500 Subject: [PATCH 43/71] feat/enh: create note from input --- backend/open_webui/routers/notes.py | 9 ++- src/lib/components/chat/MessageInput.svelte | 72 +++++++++++++++++---- src/lib/components/icons/PagePlus.svelte | 24 +++++++ src/lib/components/notes/Notes.svelte | 2 +- src/lib/components/notes/utils.ts | 6 +- 5 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/icons/PagePlus.svelte diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index cf8eb6112f..74914ae5c6 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -135,7 +135,6 @@ async def search_notes( async def create_new_note( request: Request, form_data: NoteForm, user=Depends(get_verified_user) ): - if user.role != "admin" and not has_permission( user.id, "features.notes", request.app.state.config.USER_PERMISSIONS ): @@ -187,8 +186,12 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) - write_access = has_access( - user.id, type="write", access_control=note.access_control, strict=False + write_access = ( + user.role == "admin" + or (user.id == note.user_id) + or has_access( + user.id, type="write", access_control=note.access_control, strict=False + ) ) return NoteResponse(**note.model_dump(), write_access=write_access) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 65dc33c5bf..4b91efa365 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1,14 +1,22 @@ + + diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 9204435ed3..dce991f33e 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -337,7 +337,7 @@ > - +
{$i18n.t('New Note')}
diff --git a/src/lib/components/notes/utils.ts b/src/lib/components/notes/utils.ts index 5d398ebaf2..052c48a441 100644 --- a/src/lib/components/notes/utils.ts +++ b/src/lib/components/notes/utils.ts @@ -107,7 +107,7 @@ export const downloadPdf = async (note) => { pdf.save(`${note.title}.pdf`); }; -export const createNoteHandler = async (title: string, content?: string) => { +export const createNoteHandler = async (title: string, md?: string, html?: string) => { // $i18n.t('New Note'), const res = await createNewNote(localStorage.token, { // YYYY-MM-DD @@ -115,8 +115,8 @@ export const createNoteHandler = async (title: string, content?: string) => { data: { content: { json: null, - html: content ?? '', - md: content ?? '' + html: html || md || '', + md: md || '' } }, meta: null, From 02df86784372c2aa8b5155d28794bdbab170902c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 20:52:18 -0500 Subject: [PATCH 44/71] refac --- src/lib/components/chat/MessageInput.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 4b91efa365..4633e97d6d 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -753,6 +753,8 @@ ); if (res) { + // Clear the input content saved in session storage. + sessionStorage.removeItem('chat-input'); goto(`/notes/${res.id}`); } }; From b29e7fd0bed79c06144b7d5a19b0d223ac033f98 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 21:01:39 -0500 Subject: [PATCH 45/71] refac/fix: styling --- src/lib/components/channel/ChannelInfoModal/UserList.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/channel/ChannelInfoModal/UserList.svelte b/src/lib/components/channel/ChannelInfoModal/UserList.svelte index 7f283991b1..6ba0e4e584 100644 --- a/src/lib/components/channel/ChannelInfoModal/UserList.svelte +++ b/src/lib/components/channel/ChannelInfoModal/UserList.svelte @@ -106,7 +106,7 @@
- {#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))} - - - - - {/if} - {#if (taskIds && taskIds.length > 0) || (history.currentId && history.messages[history.currentId]?.done != true) || generating}
@@ -1725,113 +1675,166 @@
- {:else if prompt === '' && files.length === 0 && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.call ?? true))} -
- - + {:else} + {#if prompt !== '' && ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} + + + {/if} + + {#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))} + + + - -
- {:else} - {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} -
- - - -
- {/if} - -
- - -
+ {/if} + + {#if prompt === '' && files.length === 0 && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.call ?? true))} +
+ + + + +
+ {:else} +
+ + + +
+ {/if} {/if}
From 3af96c9d4e880a65d679a7fb98831295d577d5c0 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 21:11:49 -0500 Subject: [PATCH 47/71] refac: styling --- src/lib/components/chat/MessageInput.svelte | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 045c886f98..0bb9f9520c 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1676,11 +1676,8 @@
{:else} - {#if prompt !== '' && ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} - + {#if prompt !== '' && !history?.currentId && ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} + +
+
+ {/if} + {#if suggestions} {#key $settings?.richTextInput ?? true} {#key $settings?.showFormattingToolbar ?? false} { prompt = content.md; inputContent = content; diff --git a/src/lib/components/common/InputModal.svelte b/src/lib/components/common/InputModal.svelte new file mode 100644 index 0000000000..d70163c9c7 --- /dev/null +++ b/src/lib/components/common/InputModal.svelte @@ -0,0 +1,79 @@ + + + +
+
+
+ {$i18n.t('Input')} +
+ +
+ +
+
+ { + value = content.md; + inputContent = content; + + onChange(content); + }} + json={true} + value={inputContent?.json} + html={inputContent?.html} + richText={$settings?.richTextInput ?? true} + messageInput={true} + showFormattingToolbar={$settings?.showFormattingToolbar ?? false} + floatingMenuPlacement={'top-start'} + insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false} + {autocomplete} + {generateAutoCompletion} + /> +
+
+
+
diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 9f352c1b02..487d1a9083 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -169,7 +169,7 @@ export let documentId = ''; - export let className = 'input-prose'; + export let className = 'input-prose min-h-fit'; export let placeholder = $i18n.t('Type here...'); let _placeholder = placeholder; @@ -1156,7 +1156,5 @@
diff --git a/src/lib/components/icons/Expand.svelte b/src/lib/components/icons/Expand.svelte new file mode 100644 index 0000000000..e11230aa37 --- /dev/null +++ b/src/lib/components/icons/Expand.svelte @@ -0,0 +1,21 @@ + + + From 6a75620fcb1cd9e2559fc56c0d398dbe0826ad80 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 21:53:34 -0500 Subject: [PATCH 49/71] refac: styling --- src/lib/components/chat/MessageInput.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 1982d70549..27fc68d593 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1237,8 +1237,8 @@ id="chat-input-container" > {#if prompt.split('\n').length > 2} -
-
+
+
-
-
+
+
{#if recording}
{:else} +
+ + {#if editing} + + {:else} + { + enhanceNoteHandler(); + }} + onChat={() => { + showPanel = true; + selectedPanel = 'chat'; + }} + > +
+ +
+
+ {/if} +
+
{ displayMediaRecord = false; @@ -1331,40 +1364,6 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
- -
- - {#if editing} - - {:else} - { - enhanceNoteHandler(); - }} - onChat={() => { - showPanel = true; - selectedPanel = 'chat'; - }} - > -
- -
-
- {/if} -
-
{/if}
From 49d54c5821169e0973ec2355ec93827aae037e85 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 9 Dec 2025 23:33:48 -0500 Subject: [PATCH 51/71] refac --- src/lib/components/chat/MessageInput.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 27fc68d593..0e0d41e4da 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1228,7 +1228,7 @@
Date: Tue, 9 Dec 2025 23:57:46 -0500 Subject: [PATCH 52/71] refac --- src/lib/components/common/DropdownOptions.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/components/common/DropdownOptions.svelte b/src/lib/components/common/DropdownOptions.svelte index 91f8607538..ecc5cc9cf8 100644 --- a/src/lib/components/common/DropdownOptions.svelte +++ b/src/lib/components/common/DropdownOptions.svelte @@ -44,7 +44,12 @@ : ' text-gray-500 dark:text-gray-400'}" type="button" on:click={() => { - value = item.value; + if (value === item.value) { + value = null; + } else { + value = item.value; + } + open = false; onChange(value); }} From 94a8439105f30203ea9d729787c9c5978f5c22a2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 10 Dec 2025 00:53:41 -0500 Subject: [PATCH 53/71] feat/enh: kb file pagination --- backend/open_webui/models/files.py | 3 + backend/open_webui/models/knowledge.py | 101 ++- backend/open_webui/models/notes.py | 3 - backend/open_webui/models/users.py | 2 +- backend/open_webui/routers/knowledge.py | 54 ++ src/lib/apis/knowledge/index.ts | 50 ++ .../workspace/Knowledge/KnowledgeBase.svelte | 585 +++++++++--------- .../KnowledgeBase/AddContentMenu.svelte | 10 +- .../Knowledge/KnowledgeBase/Files.svelte | 113 +++- 9 files changed, 602 insertions(+), 319 deletions(-) diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index 1ed743df87..79117b869a 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -238,6 +238,7 @@ class FilesTable: try: file = db.query(File).filter_by(id=id).first() file.hash = hash + file.updated_at = int(time.time()) db.commit() return FileModel.model_validate(file) @@ -249,6 +250,7 @@ class FilesTable: try: file = db.query(File).filter_by(id=id).first() file.data = {**(file.data if file.data else {}), **data} + file.updated_at = int(time.time()) db.commit() return FileModel.model_validate(file) except Exception as e: @@ -260,6 +262,7 @@ class FilesTable: try: file = db.query(File).filter_by(id=id).first() file.meta = {**(file.meta if file.meta else {}), **meta} + file.updated_at = int(time.time()) db.commit() return FileModel.model_validate(file) except Exception: diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 2c72401181..cc3ef7adee 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -7,9 +7,14 @@ import uuid from open_webui.internal.db import Base, get_db from open_webui.env import SRC_LOG_LEVELS -from open_webui.models.files import File, FileModel, FileMetadataResponse +from open_webui.models.files import ( + File, + FileModel, + FileMetadataResponse, + FileModelResponse, +) from open_webui.models.groups import Groups -from open_webui.models.users import Users, UserResponse +from open_webui.models.users import User, UserModel, Users, UserResponse from pydantic import BaseModel, ConfigDict @@ -21,6 +26,7 @@ from sqlalchemy import ( Text, JSON, UniqueConstraint, + or_, ) from open_webui.utils.access_control import has_access @@ -135,6 +141,15 @@ class KnowledgeForm(BaseModel): access_control: Optional[dict] = None +class FileUserResponse(FileModelResponse): + user: Optional[UserResponse] = None + + +class KnowledgeFileListResponse(BaseModel): + items: list[FileUserResponse] + total: int + + class KnowledgeTable: def insert_new_knowledge( self, user_id: str, form_data: KnowledgeForm @@ -232,6 +247,88 @@ class KnowledgeTable: except Exception: return [] + def search_files_by_id( + self, + knowledge_id: str, + user_id: str, + filter: dict, + skip: int = 0, + limit: int = 30, + ) -> KnowledgeFileListResponse: + try: + with get_db() as db: + query = ( + db.query(File, User) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .outerjoin(User, User.id == KnowledgeFile.user_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + ) + + if filter: + query_key = filter.get("query") + if query_key: + query = query.filter(or_(File.filename.ilike(f"%{query_key}%"))) + + view_option = filter.get("view_option") + if view_option == "created": + query = query.filter(KnowledgeFile.user_id == user_id) + elif view_option == "shared": + query = query.filter(KnowledgeFile.user_id != user_id) + + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by == "name": + if direction == "asc": + query = query.order_by(File.filename.asc()) + else: + query = query.order_by(File.filename.desc()) + elif order_by == "created_at": + if direction == "asc": + query = query.order_by(File.created_at.asc()) + else: + query = query.order_by(File.created_at.desc()) + elif order_by == "updated_at": + if direction == "asc": + query = query.order_by(File.updated_at.asc()) + else: + query = query.order_by(File.updated_at.desc()) + else: + query = query.order_by(File.updated_at.desc()) + + else: + query = query.order_by(File.updated_at.desc()) + + # Count BEFORE pagination + total = query.count() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + items = query.all() + + files = [] + for file, user in items: + files.append( + FileUserResponse( + **FileModel.model_validate(file).model_dump(), + user=( + UserResponse( + **UserModel.model_validate(user).model_dump() + ) + if user + else None + ), + ) + ) + + return KnowledgeFileListResponse(items=files, total=total) + except Exception as e: + print(e) + return KnowledgeFileListResponse(items=[], total=0) + def get_files_by_id(self, knowledge_id: str) -> list[FileModel]: try: with get_db() as db: diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index d61094b6ff..2ca932ff7f 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -302,9 +302,6 @@ class NoteTable: else: query = query.order_by(Note.updated_at.desc()) - for key, value in filter.items(): - query = query.filter(getattr(Note, key).ilike(f"%{value}%")) - # Count BEFORE pagination total = query.count() diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 86f9d011e8..5807603a89 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -5,11 +5,11 @@ from open_webui.internal.db import Base, JSONField, get_db from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL + from open_webui.models.chats import Chats from open_webui.models.groups import Groups, GroupMember from open_webui.models.channels import ChannelMember - from open_webui.utils.misc import throttle diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 3bfc961ac3..f67390518b 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -5,6 +5,7 @@ from fastapi.concurrency import run_in_threadpool import logging from open_webui.models.knowledge import ( + KnowledgeFileListResponse, Knowledges, KnowledgeForm, KnowledgeResponse, @@ -264,6 +265,59 @@ async def update_knowledge_by_id( ) +############################ +# GetKnowledgeFilesById +############################ + + +@router.get("/{id}/files", response_model=KnowledgeFileListResponse) +async def get_knowledge_files_by_id( + id: str, + query: Optional[str] = None, + view_option: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), +): + + knowledge = Knowledges.get_knowledge_by_id(id=id) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not ( + user.role == "admin" + or knowledge.user_id == user.id + or has_access(user.id, "read", knowledge.access_control) + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + page = max(page, 1) + + limit = 30 + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + if view_option: + filter["view_option"] = view_option + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + return Knowledges.search_files_by_id( + id, user.id, filter=filter, skip=skip, limit=limit + ) + + ############################ # AddFileToKnowledge ############################ diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts index c01c986a2a..98b2c1e5ec 100644 --- a/src/lib/apis/knowledge/index.ts +++ b/src/lib/apis/knowledge/index.ts @@ -132,6 +132,56 @@ export const getKnowledgeById = async (token: string, id: string) => { return res; }; +export const searchKnowledgeFilesById = async ( + token: string, + id: string, + query?: string | null = null, + viewOption?: string | null = null, + orderBy?: string | null = null, + direction?: string | null = null, + page: number = 1 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + if (viewOption) searchParams.append('view_option', viewOption); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + searchParams.append('page', page.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/knowledge/${id}/files?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 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.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + type KnowledgeUpdateForm = { name?: string; description?: string; diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index d09e608d51..1dff4bd8f8 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -31,7 +31,8 @@ removeFileFromKnowledgeById, resetKnowledgeById, updateFileFromKnowledgeById, - updateKnowledgeById + updateKnowledgeById, + searchKnowledgeFilesById } from '$lib/apis/knowledge'; import { blobToFile } from '$lib/utils'; @@ -43,22 +44,25 @@ import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte'; import SyncConfirmDialog from '../../common/ConfirmDialog.svelte'; - import RichTextInput from '$lib/components/common/RichTextInput.svelte'; - import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte'; import Drawer from '$lib/components/common/Drawer.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; import LockClosed from '$lib/components/icons/LockClosed.svelte'; import AccessControlModal from '../common/AccessControlModal.svelte'; import Search from '$lib/components/icons/Search.svelte'; - import Textarea from '$lib/components/common/Textarea.svelte'; import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte'; + import DropdownOptions from '$lib/components/common/DropdownOptions.svelte'; + import Pagination from '$lib/components/common/Pagination.svelte'; let largeScreen = true; let pane; let showSidepanel = true; - let minSize = 0; + let showAddTextContentModal = false; + let showSyncConfirmModal = false; + let showAccessControlModal = false; + + let minSize = 0; type Knowledge = { id: string; name: string; @@ -71,52 +75,88 @@ let id = null; let knowledge: Knowledge | null = null; - let query = ''; - let showAddTextContentModal = false; - let showSyncConfirmModal = false; - let showAccessControlModal = false; + let selectedFileId = null; + let selectedFile = null; + let selectedFileContent = ''; let inputFiles = null; - let filteredItems = []; - $: if (knowledge && knowledge.files) { - fuse = new Fuse(knowledge.files, { - keys: ['meta.name', 'meta.description'] - }); + let query = ''; + let viewOption = null; + let sortKey = null; + let direction = null; + + let currentPage = 1; + let fileItems = null; + let fileItemsTotal = null; + + const reset = () => { + currentPage = 1; + }; + + const init = async () => { + reset(); + await getItemsPage(); + }; + + $: if ( + knowledge !== null && + query !== undefined && + viewOption !== undefined && + sortKey !== undefined && + direction !== undefined && + currentPage !== undefined + ) { + getItemsPage(); } - $: if (fuse) { - filteredItems = query - ? fuse.search(query).map((e) => { - return e.item; - }) - : (knowledge?.files ?? []); + $: if ( + query !== undefined && + viewOption !== undefined && + sortKey !== undefined && + direction !== undefined + ) { + reset(); } - let selectedFile = null; - let selectedFileId = null; - let selectedFileContent = ''; + const getItemsPage = async () => { + if (knowledge === null) return; - // Add cache object - let fileContentCache = new Map(); + fileItems = null; + fileItemsTotal = null; - $: if (selectedFileId) { - const file = (knowledge?.files ?? []).find((file) => file.id === selectedFileId); - if (file) { - fileSelectHandler(file); - } else { - selectedFile = null; + if (sortKey === null) { + direction = null; } - } else { - selectedFile = null; - } - let fuse = null; - let debounceTimeout = null; - let mediaQuery; - let dragged = false; - let isSaving = false; + const res = await searchKnowledgeFilesById( + localStorage.token, + knowledge.id, + query, + viewOption, + sortKey, + direction, + currentPage + ).catch(() => { + return null; + }); + + if (res) { + fileItems = res.items; + fileItemsTotal = res.total; + } + return res; + }; + + const fileSelectHandler = async (file) => { + try { + selectedFile = file; + selectedFileContent = selectedFile?.data?.content || ''; + } catch (e) { + toast.error($i18n.t('Failed to load file content.')); + } + }; const createFileFromText = (name, content) => { const blob = new Blob([content], { type: 'text/plain' }); @@ -163,8 +203,7 @@ return; } - knowledge.files = [...(knowledge.files ?? []), fileItem]; - + fileItems = [...(fileItems ?? []), fileItem]; try { // If the file is an audio file, provide the language for STT. let metadata = null; @@ -184,7 +223,7 @@ if (uploadedFile) { console.log(uploadedFile); - knowledge.files = knowledge.files.map((item) => { + fileItems = fileItems.map((item) => { if (item.itemId === tempItemId) { item.id = uploadedFile.id; } @@ -197,7 +236,7 @@ if (uploadedFile.error) { console.warn('File upload warning:', uploadedFile.error); toast.warning(uploadedFile.error); - knowledge.files = knowledge.files.filter((file) => file.id !== uploadedFile.id); + fileItems = fileItems.filter((file) => file.id !== uploadedFile.id); } else { await addFileHandler(uploadedFile.id); } @@ -413,7 +452,7 @@ toast.success($i18n.t('File added successfully.')); } else { toast.error($i18n.t('Failed to add file.')); - knowledge.files = knowledge.files.filter((file) => file.id !== fileId); + fileItems = fileItems.filter((file) => file.id !== fileId); } }; @@ -436,32 +475,38 @@ } }; + let debounceTimeout = null; + let mediaQuery; + + let dragged = false; + let isSaving = false; + const updateFileContentHandler = async () => { if (isSaving) { console.log('Save operation already in progress, skipping...'); return; } + isSaving = true; + try { - const fileId = selectedFile.id; - const content = selectedFileContent; - // Clear the cache for this file since we're updating it - fileContentCache.delete(fileId); - const res = await updateFileDataContentById(localStorage.token, fileId, content).catch( - (e) => { - toast.error(`${e}`); - } - ); - const updatedKnowledge = await updateFileFromKnowledgeById( + const res = await updateFileDataContentById( localStorage.token, - id, - fileId + selectedFile.id, + selectedFileContent ).catch((e) => { toast.error(`${e}`); + return null; }); - if (res && updatedKnowledge) { - knowledge = updatedKnowledge; + + if (res) { toast.success($i18n.t('File content updated successfully.')); + + selectedFileId = null; + selectedFile = null; + selectedFileContent = ''; + + await init(); } } finally { isSaving = false; @@ -504,29 +549,6 @@ } }; - const fileSelectHandler = async (file) => { - try { - selectedFile = file; - - // Check cache first - if (fileContentCache.has(file.id)) { - selectedFileContent = fileContentCache.get(file.id); - return; - } - - const response = await getFileById(localStorage.token, file.id); - if (response) { - selectedFileContent = response.data.content; - // Cache the content - fileContentCache.set(file.id, response.data.content); - } else { - toast.error($i18n.t('No content found in file.')); - } - } catch (e) { - toast.error($i18n.t('Failed to load file content.')); - } - }; - const onDragOver = (e) => { e.preventDefault(); @@ -705,32 +727,42 @@ }} /> -
+
{#if id && knowledge} { changeDebounceHandler(); }} accessRoles={['read', 'write']} /> -
+
-
-
+
+
{ changeDebounceHandler(); }} /> + +
+ {#if (knowledge?.files ?? []).length} +
+ {$i18n.t('{{count}} files', { + count: (knowledge?.files ?? []).length + })} +
+ {/if} +
@@ -750,7 +782,7 @@
-
+
-
- {#if largeScreen} -
- {#if selectedFile} -
-
- {#if !showSidepanel} -
- -
- {/if} +
+
+
+
+ +
+ { + selectedFileId = null; + }} + /> - - -
- -
-
- -
- {#key selectedFile.id} -