diff --git a/CHANGELOG.md b/CHANGELOG.md index 16866a7934..406d58cda7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.2] - 2024-06-10 + +### Added + +- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries. +- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs. +- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese. + +### Fixed + +- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication. +- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback. +- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors. + ## [0.3.1] - 2024-06-09 ### Fixed diff --git a/README.md b/README.md index 7a6df25925..5bce3ad06e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature- - 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query. -- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience. +- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, and `Serply` and inject the results directly into your chat experience. - 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions. diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 8816321b37..99730ad7c0 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -69,7 +69,7 @@ from apps.rag.search.main import SearchResult from apps.rag.search.searxng import search_searxng from apps.rag.search.serper import search_serper from apps.rag.search.serpstack import search_serpstack - +from apps.rag.search.serply import search_serply from utils.misc import ( calculate_sha256, @@ -115,6 +115,7 @@ from config import ( SERPSTACK_API_KEY, SERPSTACK_HTTPS, SERPER_API_KEY, + SERPLY_API_KEY, RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_EMBEDDING_OPENAI_BATCH_SIZE, @@ -167,6 +168,7 @@ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS app.state.config.SERPER_API_KEY = SERPER_API_KEY +app.state.config.SERPLY_API_KEY = SERPLY_API_KEY app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS @@ -394,6 +396,7 @@ async def get_rag_config(user=Depends(get_admin_user)): "serpstack_api_key": app.state.config.SERPSTACK_API_KEY, "serpstack_https": app.state.config.SERPSTACK_HTTPS, "serper_api_key": app.state.config.SERPER_API_KEY, + "serply_api_key": app.state.config.SERPLY_API_KEY, "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, }, @@ -421,6 +424,7 @@ class WebSearchConfig(BaseModel): serpstack_api_key: Optional[str] = None serpstack_https: Optional[bool] = None serper_api_key: Optional[str] = None + serply_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None @@ -471,6 +475,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_ app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key + app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests @@ -499,6 +504,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_ "serpstack_api_key": app.state.config.SERPSTACK_API_KEY, "serpstack_https": app.state.config.SERPSTACK_HTTPS, "serper_api_key": app.state.config.SERPER_API_KEY, + "serply_api_key": app.state.config.SERPLY_API_KEY, "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, }, @@ -746,6 +752,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]: - BRAVE_SEARCH_API_KEY - SERPSTACK_API_KEY - SERPER_API_KEY + - SERPLY_API_KEY Args: query (str): The query to search for @@ -804,6 +811,15 @@ def search_web(engine: str, query: str) -> list[SearchResult]: ) else: raise Exception("No SERPER_API_KEY found in environment variables") + elif engine == "serply": + if app.state.config.SERPLY_API_KEY: + return search_serply( + app.state.config.SERPLY_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + ) + else: + raise Exception("No SERPLY_API_KEY found in environment variables") else: raise Exception("No search engine API key found in environment variables") @@ -811,6 +827,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]: @app.post("/web/search") def store_web_search(form_data: SearchForm, user=Depends(get_current_user)): try: + logging.info(f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}") web_results = search_web( app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query ) diff --git a/backend/apps/rag/search/serply.py b/backend/apps/rag/search/serply.py new file mode 100644 index 0000000000..12d5e51d26 --- /dev/null +++ b/backend/apps/rag/search/serply.py @@ -0,0 +1,68 @@ +import json +import logging + +import requests +from urllib.parse import urlencode + +from apps.rag.search.main import SearchResult +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_serply( + api_key: str, + query: str, + count: int, + hl: str = "us", + limit: int = 10, + device_type: str = "desktop", + proxy_location: str = "US" + ) -> list[SearchResult]: + """Search using serper.dev's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serply.io API key + query (str): The query to search for + hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) + limit (int): The maximum number of results to return [10-100, defaults to 10] + """ + log.info("Searching with Serply") + + url = "https://api.serply.io/v1/search/" + + query_payload = { + "q": query, + "language": "en", + "num": limit, + "gl": proxy_location.upper(), + "hl": hl.lower() + } + + url = f"{url}{urlencode(query_payload)}" + headers = { + "X-API-KEY": api_key, + "X-User-Agent": device_type, + "User-Agent": "open-webui", + "X-Proxy-Location": proxy_location + } + + response = requests.request("GET", url, headers=headers) + response.raise_for_status() + + json_response = response.json() + log.info(f"results from serply search: {json_response}") + + results = sorted( + json_response.get("results", []), key=lambda x: x.get("realPosition", 0) + ) + + return [ + SearchResult( + link=result["link"], + title=result.get("title"), + snippet=result.get("description"), + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/testdata/serply.json b/backend/apps/rag/search/testdata/serply.json new file mode 100644 index 0000000000..0fc2a31e4d --- /dev/null +++ b/backend/apps/rag/search/testdata/serply.json @@ -0,0 +1,206 @@ +{ + "ads": [], + "ads_count": 0, + "answers": [], + "results": [ + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "subdomains": [ + { + "title": "Support", + "link": "https://support.apple.com/", + "description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair" + }, + { + "title": "Store", + "link": "https://www.apple.com/store", + "description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..." + }, + { + "title": "Mac", + "link": "https://www.apple.com/mac/", + "description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini" + }, + { + "title": "iPad", + "link": "https://www.apple.com/ipad/", + "description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..." + }, + { + "title": "Watch", + "link": "https://www.apple.com/watch/", + "description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..." + } + ], + "realPosition": 1 + }, + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "realPosition": 2 + }, + { + "title": "Apple Inc.", + "link": "https://en.wikipedia.org/wiki/Apple_Inc.", + "description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...", + "additional_links": [ + { + "text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "History", + "href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." + }, + { + "text": "List of Apple products", + "href": "https://en.wikipedia.org/wiki/List_of_Apple_products" + }, + { + "text": "Litigation involving Apple Inc.", + "href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc." + }, + { + "text": "Apple Park", + "href": "https://en.wikipedia.org/wiki/Apple_Park" + } + ], + "cite": { + "domain": "https://en.wikipedia.org › wiki › Apple_Inc", + "span": " › wiki › Apple_Inc" + }, + "realPosition": 3 + }, + { + "title": "Apple Inc. (AAPL) Company Profile & Facts", + "link": "https://finance.yahoo.com/quote/AAPL/profile/", + "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...", + "additional_links": [ + { + "text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile", + "href": "https://finance.yahoo.com/quote/AAPL/profile/" + } + ], + "cite": { + "domain": "https://finance.yahoo.com › quote › AAPL › profile", + "span": " › quote › AAPL › profile" + }, + "realPosition": 4 + }, + { + "title": "Apple Inc - Company Profile and News", + "link": "https://www.bloomberg.com/profile/company/AAPL:US", + "description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...", + "additional_links": [ + { + "text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + }, + { + "text": "", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + } + ], + "cite": { + "domain": "https://www.bloomberg.com › company › AAPL:US", + "span": " › company › AAPL:US" + }, + "realPosition": 5 + }, + { + "title": "Apple Inc. | History, Products, Headquarters, & Facts", + "link": "https://www.britannica.com/money/Apple-Inc", + "description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...", + "additional_links": [ + { + "text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc", + "href": "https://www.britannica.com/money/Apple-Inc" + }, + { + "text": "", + "href": "https://www.britannica.com/money/Apple-Inc" + } + ], + "cite": { + "domain": "https://www.britannica.com › money › Apple-Inc", + "span": " › money › Apple-Inc" + }, + "realPosition": 6 + } + ], + "shopping_ads": [], + "places": [ + { + "title": "Apple Inc." + }, + { + "title": "Apple Inc" + }, + { + "title": "Apple Inc" + } + ], + "related_searches": { + "images": [], + "text": [ + { + "title": "apple inc full form", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE" + }, + { + "title": "apple company history", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE" + }, + { + "title": "apple store", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE" + }, + { + "title": "apple id", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE" + }, + { + "title": "apple inc industry", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE" + }, + { + "title": "apple login", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE" + } + ] + }, + "image_results": [], + "carousel": [], + "total": 2450000000, + "knowledge_graph": "", + "related_questions": [ + "What does the Apple Inc do?", + "Why did Apple change to Apple Inc?", + "Who owns Apple Inc.?", + "What is Apple Inc best known for?" + ], + "carousel_count": 0, + "ts": 2.491065263748169, + "device_type": null +} diff --git a/backend/config.py b/backend/config.py index 27c4c12770..df52a4b69f 100644 --- a/backend/config.py +++ b/backend/config.py @@ -308,8 +308,9 @@ frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png" if frontend_favicon.exists(): try: shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") - except PermissionError: - logging.error(f"No write permission to {STATIC_DIR / 'favicon.png'}") + except Exception as e: + logging.error(f"An error occurred: {e}") + else: logging.warning(f"Frontend favicon not found at {frontend_favicon}") @@ -915,6 +916,12 @@ SERPER_API_KEY = PersistentConfig( os.getenv("SERPER_API_KEY", ""), ) +SERPLY_API_KEY = PersistentConfig( + "SERPLY_API_KEY", + "rag.web.search.serply_api_key", + os.getenv("SERPLY_API_KEY", ""), +) + RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig( "RAG_WEB_SEARCH_RESULT_COUNT", diff --git a/package-lock.json b/package-lock.json index 4451392058..f2eedf51ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.3.1", + "version": "0.3.2", "dependencies": { "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", diff --git a/package.json b/package.json index ebce684487..7ec0a5d72b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.3.1", + "version": "0.3.2", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -71,4 +71,4 @@ "tippy.js": "^6.3.7", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 7013759fa2..eeafdce15a 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -11,7 +11,7 @@ export let saveHandler: Function; let webConfig = null; - let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper']; + let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper', 'serply']; let youtubeLanguage = 'en'; let youtubeTranslation = null; @@ -188,6 +188,24 @@ + {:else if webConfig.search.engine === 'serply'} +
+
+ {$i18n.t('Serply API Key')} +
+ +
+
+ +
+
+
{/if} {/if} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index b09faf1103..47e1075df3 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -327,6 +327,9 @@ chatTextAreaElement.style.height = ''; } + const _files = JSON.parse(JSON.stringify(files)); + files = []; + prompt = ''; // Create user message @@ -338,7 +341,7 @@ role: 'user', user: _user ?? undefined, content: userPrompt, - files: files.length > 0 ? files : undefined, + files: _files.length > 0 ? _files : undefined, timestamp: Math.floor(Date.now() / 1000), // Unix epoch models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx) }; @@ -355,32 +358,6 @@ // Wait until history/message have been updated await tick(); - // Create new chat if only one message in messages - if (messages.length == 1) { - if ($settings.saveChatHistory ?? true) { - chat = await createNewChat(localStorage.token, { - id: $chatId, - title: $i18n.t('New Chat'), - models: selectedModels, - system: $settings.system ?? undefined, - options: { - ...($settings.params ?? {}) - }, - messages: messages, - history: history, - tags: [], - timestamp: Date.now() - }); - await chats.set(await getChatList(localStorage.token)); - await chatId.set(chat.id); - } else { - await chatId.set('local'); - } - await tick(); - } - - files = []; - // Send prompt _responses = await sendPrompt(userPrompt, userMessageId); } @@ -390,15 +367,78 @@ const sendPrompt = async (prompt, parentId, modelId = null) => { let _responses = []; + + // If modelId is provided, use it, else use selected model + let selectedModelIds = modelId + ? [modelId] + : atSelectedModel !== undefined + ? [atSelectedModel.id] + : selectedModels; + + // Create response messages for each selected model + const responseMessageIds = {}; + for (const modelId of selectedModelIds) { + const model = $models.filter((m) => m.id === modelId).at(0); + + if (model) { + let responseMessageId = uuidv4(); + let responseMessage = { + parentId: parentId, + id: responseMessageId, + childrenIds: [], + role: 'assistant', + content: '', + model: model.id, + modelName: model.name ?? model.id, + userContext: null, + timestamp: Math.floor(Date.now() / 1000) // Unix epoch + }; + + // Add message to history and Set currentId to messageId + history.messages[responseMessageId] = responseMessage; + history.currentId = responseMessageId; + + // Append messageId to childrenIds of parent message + if (parentId !== null) { + history.messages[parentId].childrenIds = [ + ...history.messages[parentId].childrenIds, + responseMessageId + ]; + } + + responseMessageIds[modelId] = responseMessageId; + } + } + await tick(); + + // Create new chat if only one message in messages + if (messages.length == 2) { + if ($settings.saveChatHistory ?? true) { + chat = await createNewChat(localStorage.token, { + id: $chatId, + title: $i18n.t('New Chat'), + models: selectedModels, + system: $settings.system ?? undefined, + options: { + ...($settings.params ?? {}) + }, + messages: messages, + history: history, + tags: [], + timestamp: Date.now() + }); + await chats.set(await getChatList(localStorage.token)); + await chatId.set(chat.id); + } else { + await chatId.set('local'); + } + await tick(); + } + const _chatId = JSON.parse(JSON.stringify($chatId)); await Promise.all( - (modelId - ? [modelId] - : atSelectedModel !== undefined - ? [atSelectedModel.id] - : selectedModels - ).map(async (modelId) => { + selectedModelIds.map(async (modelId) => { console.log('modelId', modelId); const model = $models.filter((m) => m.id === modelId).at(0); @@ -416,33 +456,8 @@ ); } - // Create response message - let responseMessageId = uuidv4(); - let responseMessage = { - parentId: parentId, - id: responseMessageId, - childrenIds: [], - role: 'assistant', - content: '', - model: model.id, - modelName: model.name ?? model.id, - userContext: null, - timestamp: Math.floor(Date.now() / 1000) // Unix epoch - }; - - // Add message to history and Set currentId to messageId - history.messages[responseMessageId] = responseMessage; - history.currentId = responseMessageId; - - // Append messageId to childrenIds of parent message - if (parentId !== null) { - history.messages[parentId].childrenIds = [ - ...history.messages[parentId].childrenIds, - responseMessageId - ]; - } - - await tick(); + let responseMessageId = responseMessageIds[modelId]; + let responseMessage = history.messages[responseMessageId]; let userContext = null; if ($settings?.memory ?? false) { @@ -451,7 +466,6 @@ toast.error(error); return null; }); - if (res) { if (res.documents[0].length > 0) { userContext = res.documents.reduce((acc, doc, index) => { @@ -477,7 +491,6 @@ } let _response = null; - if (model?.owned_by === 'openai') { _response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId); } else if (model) { @@ -502,11 +515,13 @@ const getWebSearchResults = async (model: string, parentId: string, responseId: string) => { const responseMessage = history.messages[responseId]; - responseMessage.status = { - done: false, - action: 'web_search', - description: $i18n.t('Generating search query') - }; + responseMessage.statusHistory = [ + { + done: false, + action: 'web_search', + description: $i18n.t('Generating search query') + } + ]; messages = messages; const prompt = history.messages[parentId].content; @@ -519,19 +534,21 @@ if (!searchQuery) { toast.warning($i18n.t('No search query generated')); - responseMessage.status = { - ...responseMessage.status, + responseMessage.statusHistory.push({ done: true, error: true, + action: 'web_search', description: 'No search query generated' - }; + }); + messages = messages; } - responseMessage.status = { - ...responseMessage.status, - description: $i18n.t("Searching the web for '{{searchQuery}}'", { searchQuery }) - }; + responseMessage.statusHistory.push({ + done: false, + action: 'web_search', + description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery }) + }); messages = messages; const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => { @@ -542,12 +559,13 @@ }); if (results) { - responseMessage.status = { - ...responseMessage.status, + responseMessage.statusHistory.push({ done: true, + action: 'web_search', description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }), + query: searchQuery, urls: results.filenames - }; + }); if (responseMessage?.files ?? undefined === undefined) { responseMessage.files = []; @@ -562,12 +580,12 @@ messages = messages; } else { - responseMessage.status = { - ...responseMessage.status, + responseMessage.statusHistory.push({ done: true, error: true, + action: 'web_search', description: 'No search results found' - }; + }); messages = messages; } }; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 372c26d104..b3ceb3e91d 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -29,6 +29,7 @@ import InputMenu from './MessageInput/InputMenu.svelte'; import Headphone from '../icons/Headphone.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte'; + import { transcribeAudio } from '$lib/apis/audio'; const i18n = getContext('i18n'); diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte index 4fb7445736..295750c9ee 100644 --- a/src/lib/components/chat/MessageInput/CallOverlay.svelte +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -205,7 +205,7 @@ if (_responses.at(0)) { const content = _responses[0]; - if (content) { + if ((content ?? '').trim() !== '') { assistantSpeakingHandler(content); } } diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 8c9ac7b4c5..281f7671f0 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -211,93 +211,98 @@ speaking = null; speakingIdx = null; } else { - speaking = true; + if ((message?.content ?? '').trim() !== '') { + speaking = true; - if ($config.audio.tts.engine === 'openai') { - loadingSpeech = true; + if ($config.audio.tts.engine === 'openai') { + loadingSpeech = true; - const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => { - const lastIndex = mergedTexts.length - 1; - if (lastIndex >= 0) { - const previousText = mergedTexts[lastIndex]; - const wordCount = previousText.split(/\s+/).length; - if (wordCount < 2) { - mergedTexts[lastIndex] = previousText + ' ' + currentText; + const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => { + const lastIndex = mergedTexts.length - 1; + if (lastIndex >= 0) { + const previousText = mergedTexts[lastIndex]; + const wordCount = previousText.split(/\s+/).length; + if (wordCount < 2) { + mergedTexts[lastIndex] = previousText + ' ' + currentText; + } else { + mergedTexts.push(currentText); + } } else { mergedTexts.push(currentText); } - } else { - mergedTexts.push(currentText); + return mergedTexts; + }, []); + + console.log(sentences); + + sentencesAudio = sentences.reduce((a, e, i, arr) => { + a[i] = null; + return a; + }, {}); + + let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately + + for (const [idx, sentence] of sentences.entries()) { + const res = await synthesizeOpenAISpeech( + localStorage.token, + $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, + sentence + ).catch((error) => { + toast.error(error); + + speaking = null; + loadingSpeech = false; + + return null; + }); + + if (res) { + const blob = await res.blob(); + const blobUrl = URL.createObjectURL(blob); + const audio = new Audio(blobUrl); + sentencesAudio[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } } - return mergedTexts; - }, []); + } else { + let voices = []; + const getVoicesLoop = setInterval(async () => { + voices = await speechSynthesis.getVoices(); + if (voices.length > 0) { + clearInterval(getVoicesLoop); - console.log(sentences); + const voice = + voices + ?.filter( + (v) => + v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) + ) + ?.at(0) ?? undefined; - sentencesAudio = sentences.reduce((a, e, i, arr) => { - a[i] = null; - return a; - }, {}); + console.log(voice); - let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately + const speak = new SpeechSynthesisUtterance(message.content); - for (const [idx, sentence] of sentences.entries()) { - const res = await synthesizeOpenAISpeech( - localStorage.token, - $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, - sentence - ).catch((error) => { - toast.error(error); + console.log(speak); - speaking = null; - loadingSpeech = false; + speak.onend = () => { + speaking = null; + if ($settings.conversationMode) { + document.getElementById('voice-input-button')?.click(); + } + }; - return null; - }); + if (voice) { + speak.voice = voice; + } - if (res) { - const blob = await res.blob(); - const blobUrl = URL.createObjectURL(blob); - const audio = new Audio(blobUrl); - sentencesAudio[idx] = audio; - loadingSpeech = false; - lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); - } + speechSynthesis.speak(speak); + } + }, 100); } } else { - let voices = []; - const getVoicesLoop = setInterval(async () => { - voices = await speechSynthesis.getVoices(); - if (voices.length > 0) { - clearInterval(getVoicesLoop); - - const voice = - voices - ?.filter( - (v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) - ) - ?.at(0) ?? undefined; - - console.log(voice); - - const speak = new SpeechSynthesisUtterance(message.content); - - console.log(speak); - - speak.onend = () => { - speaking = null; - if ($settings.conversationMode) { - document.getElementById('voice-input-button')?.click(); - } - }; - - if (voice) { - speak.voice = voice; - } - - speechSynthesis.speak(speak); - } - }, 100); + toast.error('No content to speak'); } } }; @@ -415,26 +420,29 @@ class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line" >
- {#if message?.status} + {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0} + {@const status = ( + message?.statusHistory ?? [...(message?.status ? [message?.status] : [])] + ).at(-1)}
- {#if message?.status?.done === false} + {#if status.done === false}
{/if} - {#if message?.status?.action === 'web_search' && message?.status?.urls} - + {#if status?.action === 'web_search' && status?.urls} +
- {message.status.description} + {status?.description}
{:else}
- {message.status.description} + {status?.description}
{/if} diff --git a/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte index dc233c6d4f..5281080362 100644 --- a/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte @@ -1,10 +1,11 @@ @@ -27,11 +28,45 @@ class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" transition={slide} > - {#each urls as url, urlIdx} + {#if status?.query} + +
+ + +
+ {status.query} +
+
+ +
+ + + + +
+
+ {/if} + + {#each status.urls as url, urlIdx} diff --git a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte index 05a6aed140..b983fc27fd 100644 --- a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte +++ b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte @@ -5,21 +5,23 @@ const i18n = getContext('i18n'); + export let admin = false; + export let params = { // Advanced - seed: 0, + seed: null, stop: null, - temperature: '', - frequency_penalty: '', - repeat_last_n: '', - mirostat: '', - mirostat_eta: '', - mirostat_tau: '', - top_k: '', - top_p: '', - tfs_z: '', - num_ctx: '', - max_tokens: '', + temperature: null, + frequency_penalty: null, + repeat_last_n: null, + mirostat: null, + mirostat_eta: null, + mirostat_tau: null, + top_k: null, + top_p: null, + tfs_z: null, + num_ctx: null, + max_tokens: null, use_mmap: null, use_mlock: null, num_thread: null, @@ -112,10 +114,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.temperature = (params?.temperature ?? '') === '' ? 0.8 : ''; + params.temperature = (params?.temperature ?? null) === null ? 0.8 : null; }} > - {#if (params?.temperature ?? '') === ''} + {#if (params?.temperature ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -123,7 +125,7 @@
- {#if (params?.temperature ?? '') !== ''} + {#if (params?.temperature ?? null) !== null}
@@ -158,10 +160,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.mirostat = (params?.mirostat ?? '') === '' ? 0 : ''; + params.mirostat = (params?.mirostat ?? null) === null ? 0 : null; }} > - {#if (params?.mirostat ?? '') === ''} + {#if (params?.mirostat ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -169,7 +171,7 @@
- {#if (params?.mirostat ?? '') !== ''} + {#if (params?.mirostat ?? null) !== null}
{ - params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : ''; + params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null; }} > - {#if (params?.mirostat_eta ?? '') === ''} + {#if (params?.mirostat_eta ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -215,7 +217,7 @@
- {#if (params?.mirostat_eta ?? '') !== ''} + {#if (params?.mirostat_eta ?? null) !== null}
@@ -250,10 +252,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : ''; + params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null; }} > - {#if (params?.mirostat_tau ?? '') === ''} + {#if (params?.mirostat_tau ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -261,7 +263,7 @@
- {#if (params?.mirostat_tau ?? '') !== ''} + {#if (params?.mirostat_tau ?? null) !== null}
@@ -296,10 +298,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.top_k = (params?.top_k ?? '') === '' ? 40 : ''; + params.top_k = (params?.top_k ?? null) === null ? 40 : null; }} > - {#if (params?.top_k ?? '') === ''} + {#if (params?.top_k ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -307,7 +309,7 @@ - {#if (params?.top_k ?? '') !== ''} + {#if (params?.top_k ?? null) !== null}
@@ -342,10 +344,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.top_p = (params?.top_p ?? '') === '' ? 0.9 : ''; + params.top_p = (params?.top_p ?? null) === null ? 0.9 : null; }} > - {#if (params?.top_p ?? '') === ''} + {#if (params?.top_p ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -353,7 +355,7 @@ - {#if (params?.top_p ?? '') !== ''} + {#if (params?.top_p ?? null) !== null}
@@ -388,10 +390,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : ''; + params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null; }} > - {#if (params?.frequency_penalty ?? '') === ''} + {#if (params?.frequency_penalty ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -399,7 +401,7 @@ - {#if (params?.frequency_penalty ?? '') !== ''} + {#if (params?.frequency_penalty ?? null) !== null}
@@ -434,10 +436,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : ''; + params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null; }} > - {#if (params?.repeat_last_n ?? '') === ''} + {#if (params?.repeat_last_n ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -445,7 +447,7 @@ - {#if (params?.repeat_last_n ?? '') !== ''} + {#if (params?.repeat_last_n ?? null) !== null}
{ - params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : ''; + params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null; }} > - {#if (params?.tfs_z ?? '') === ''} + {#if (params?.tfs_z ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -491,7 +493,7 @@
- {#if (params?.tfs_z ?? '') !== ''} + {#if (params?.tfs_z ?? null) !== null}
@@ -526,10 +528,10 @@ class="p-1 px-3 text-xs flex rounded transition" type="button" on:click={() => { - params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : ''; + params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null; }} > - {#if (params?.num_ctx ?? '') === ''} + {#if (params?.num_ctx ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -537,7 +539,7 @@
- {#if (params?.num_ctx ?? '') !== ''} + {#if (params?.num_ctx ?? null) !== null}
{ - params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : ''; + params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null; }} > - {#if (params?.max_tokens ?? '') === ''} + {#if (params?.max_tokens ?? null) === null} {$i18n.t('Default')} {:else} {$i18n.t('Custom')} @@ -582,7 +584,7 @@
- {#if (params?.max_tokens ?? '') !== ''} + {#if (params?.max_tokens ?? null) !== null}
-
-
-
{$i18n.t('use_mmap (Ollama)')}
+ {#if admin} +
+
+
{$i18n.t('use_mmap (Ollama)')}
- -
-
- -
-
-
{$i18n.t('use_mlock (Ollama)')}
- - -
-
- -
-
-
{$i18n.t('num_thread (Ollama)')}
- - -
- - {#if (params?.num_thread ?? null) !== null} -
-
- -
-
- -
+
- {/if} -
- -
-
-
{$i18n.t('Template')}
- -
- {#if (params?.template ?? null) !== null} -
-
-