diff --git a/CHANGELOG.md b/CHANGELOG.md index 0008d21ec..758a45979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ 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.24] - 2024-09-24 + +### Added + +- **🚀 Rendering Optimization**: Significantly improved message rendering performance, enhancing user experience and webui responsiveness. +- **💖 Favorite Response Feature in Chat Overview**: Users can now mark responses as favorite directly from the chat overview, enhancing ease of retrieval and organization of preferred responses. +- **💬 Create Message Pairs with Shortcut**: Implemented creation of new message pairs using Cmd/Ctrl+Shift+Enter, making conversation editing faster and more intuitive. +- **🌍 Expanded User Prompt Variables**: Added weekday, timezone, and language information variables to user prompts to match system prompt variables. +- **🎵 Enhanced Audio Support**: Now includes support for 'audio/x-m4a' files, broadening compatibility with audio content within the platform. +- **🔏 Model URL Search Parameter**: Added an ability to select a model directly via URL parameters, streamlining navigation and model access. +- **📄 Enhanced PDF Citations**: PDF citations now open at the associated page, streamlining reference checks and document handling. +- **🔧Use of Redis in Sockets**: Enhanced socket implementation to fully support Redis, enabling effective stateless instances suitable for scalable load balancing. +- **🌍 Stream Individual Model Responses**: Allows specific models to have individualized streaming settings, enhancing performance and customization. +- **🕒 Display Model Hash and Last Modified Timestamp for Ollama Models**: Provides critical model details directly in the Models workspace for enhanced tracking. +- **❗ Update Info Notification for Admins**: Ensures administrators receive immediate updates upon login, keeping them informed of the latest changes and system statuses. + +### Fixed + +- **🗑️ Temporary File Handling On Windows**: Fixed an issue causing errors when accessing a temporary file being used by another process, Tools & Functions should now work as intended. +- **🔓 Authentication Toggle Issue**: Resolved the malfunction where setting 'WEBUI_AUTH=False' did not appropriately disable authentication, ensuring that user experience and system security settings function as configured. +- **🔧 Save As Copy Issue for Many Model Chats**: Resolved an error preventing users from save messages as copies in many model chats. +- **🔒 Sidebar Closure on Mobile**: Resolved an issue where the mobile sidebar remained open after menu engagement, improving user interface responsivity and comfort. +- **🛡️ Tooltip XSS Vulnerability**: Resolved a cross-site scripting (XSS) issue within tooltips, ensuring enhanced security and data integrity during user interactions. + +### Changed + +- **↩️ Deprecated Interface Stream Response Settings**: Moved to advanced parameters to streamline interface settings and enhance user clarity. +- **⚙️ Renamed 'speedRate' to 'playbackRate'**: Standardizes terminology, improving usability and understanding in media settings. + ## [0.3.23] - 2024-09-21 ### Added diff --git a/backend/open_webui/apps/audio/main.py b/backend/open_webui/apps/audio/main.py index 0eee533bd..a1e6e94fa 100644 --- a/backend/open_webui/apps/audio/main.py +++ b/backend/open_webui/apps/audio/main.py @@ -360,7 +360,7 @@ def transcribe( ): log.info(f"file.content_type: {file.content_type}") - if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg"]: + if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, diff --git a/backend/open_webui/apps/socket/main.py b/backend/open_webui/apps/socket/main.py index c353c8e6f..7883b7d8b 100644 --- a/backend/open_webui/apps/socket/main.py +++ b/backend/open_webui/apps/socket/main.py @@ -1,6 +1,9 @@ import asyncio - import socketio +import logging +import sys +import time + from open_webui.apps.webui.models.users import Users from open_webui.env import ( ENABLE_WEBSOCKET_SUPPORT, @@ -8,6 +11,17 @@ from open_webui.env import ( WEBSOCKET_REDIS_URL, ) from open_webui.utils.utils import decode_token +from open_webui.apps.socket.utils import RedisDict + +from open_webui.env import ( + GLOBAL_LOG_LEVEL, + SRC_LOG_LEVELS, +) + + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["SOCKET"]) if WEBSOCKET_MANAGER == "redis": @@ -38,13 +52,72 @@ app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io") # Dictionary to maintain the user pool -SESSION_POOL = {} -USER_POOL = {} -USAGE_POOL = {} +if WEBSOCKET_MANAGER == "redis": + SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL) + USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL) + USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL) +else: + SESSION_POOL = {} + USER_POOL = {} + USAGE_POOL = {} + + # Timeout duration in seconds TIMEOUT_DURATION = 3 +async def periodic_usage_pool_cleanup(): + while True: + now = int(time.time()) + for model_id, connections in list(USAGE_POOL.items()): + # Creating a list of sids to remove if they have timed out + expired_sids = [ + sid + for sid, details in connections.items() + if now - details["updated_at"] > TIMEOUT_DURATION + ] + + for sid in expired_sids: + del connections[sid] + + if not connections: + log.debug(f"Cleaning up model {model_id} from usage pool") + del USAGE_POOL[model_id] + else: + USAGE_POOL[model_id] = connections + + # Emit updated usage information after cleaning + await sio.emit("usage", {"models": get_models_in_use()}) + + await asyncio.sleep(TIMEOUT_DURATION) + + +# Start the cleanup task when your app starts +asyncio.create_task(periodic_usage_pool_cleanup()) + + +def get_models_in_use(): + # List models that are currently in use + models_in_use = list(USAGE_POOL.keys()) + return models_in_use + + +@sio.on("usage") +async def usage(sid, data): + model_id = data["model"] + # Record the timestamp for the last update + current_time = int(time.time()) + + # Store the new usage data and task + USAGE_POOL[model_id] = { + **(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}), + sid: {"updated_at": current_time}, + } + + # Broadcast the usage data to all clients + await sio.emit("usage", {"models": get_models_in_use()}) + + @sio.event async def connect(sid, environ, auth): user = None @@ -62,8 +135,7 @@ async def connect(sid, environ, auth): USER_POOL[user.id] = [sid] # print(f"user {user.name}({user.id}) connected with session ID {sid}") - - await sio.emit("user-count", {"count": len(set(USER_POOL))}) + await sio.emit("user-count", {"count": len(USER_POOL.items())}) await sio.emit("usage", {"models": get_models_in_use()}) @@ -91,65 +163,12 @@ async def user_join(sid, data): # print(f"user {user.name}({user.id}) connected with session ID {sid}") - await sio.emit("user-count", {"count": len(set(USER_POOL))}) + await sio.emit("user-count", {"count": len(USER_POOL.items())}) @sio.on("user-count") async def user_count(sid): - await sio.emit("user-count", {"count": len(set(USER_POOL))}) - - -def get_models_in_use(): - # Aggregate all models in use - models_in_use = [] - for model_id, data in USAGE_POOL.items(): - models_in_use.append(model_id) - - return models_in_use - - -@sio.on("usage") -async def usage(sid, data): - model_id = data["model"] - - # Cancel previous callback if there is one - if model_id in USAGE_POOL: - USAGE_POOL[model_id]["callback"].cancel() - - # Store the new usage data and task - - if model_id in USAGE_POOL: - USAGE_POOL[model_id]["sids"].append(sid) - USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"])) - - else: - USAGE_POOL[model_id] = {"sids": [sid]} - - # Schedule a task to remove the usage data after TIMEOUT_DURATION - USAGE_POOL[model_id]["callback"] = asyncio.create_task( - remove_after_timeout(sid, model_id) - ) - - # Broadcast the usage data to all clients - await sio.emit("usage", {"models": get_models_in_use()}) - - -async def remove_after_timeout(sid, model_id): - try: - await asyncio.sleep(TIMEOUT_DURATION) - if model_id in USAGE_POOL: - # print(USAGE_POOL[model_id]["sids"]) - USAGE_POOL[model_id]["sids"].remove(sid) - USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"])) - - if len(USAGE_POOL[model_id]["sids"]) == 0: - del USAGE_POOL[model_id] - - # Broadcast the usage data to all clients - await sio.emit("usage", {"models": get_models_in_use()}) - except asyncio.CancelledError: - # Task was cancelled due to new 'usage' event - pass + await sio.emit("user-count", {"count": len(USER_POOL.items())}) @sio.event @@ -158,7 +177,7 @@ async def disconnect(sid): user_id = SESSION_POOL[sid] del SESSION_POOL[sid] - USER_POOL[user_id].remove(sid) + USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid] if len(USER_POOL[user_id]) == 0: del USER_POOL[user_id] diff --git a/backend/open_webui/apps/socket/utils.py b/backend/open_webui/apps/socket/utils.py new file mode 100644 index 000000000..1862ff439 --- /dev/null +++ b/backend/open_webui/apps/socket/utils.py @@ -0,0 +1,59 @@ +import json +import redis + + +class RedisDict: + def __init__(self, name, redis_url): + self.name = name + self.redis = redis.Redis.from_url(redis_url, decode_responses=True) + + def __setitem__(self, key, value): + serialized_value = json.dumps(value) + self.redis.hset(self.name, key, serialized_value) + + def __getitem__(self, key): + value = self.redis.hget(self.name, key) + if value is None: + raise KeyError(key) + return json.loads(value) + + def __delitem__(self, key): + result = self.redis.hdel(self.name, key) + if result == 0: + raise KeyError(key) + + def __contains__(self, key): + return self.redis.hexists(self.name, key) + + def __len__(self): + return self.redis.hlen(self.name) + + def keys(self): + return self.redis.hkeys(self.name) + + def values(self): + return [json.loads(v) for v in self.redis.hvals(self.name)] + + def items(self): + return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()] + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def clear(self): + self.redis.delete(self.name) + + def update(self, other=None, **kwargs): + if other is not None: + for k, v in other.items() if hasattr(other, "items") else other: + self[k] = v + for k, v in kwargs.items(): + self[k] = v + + def setdefault(self, key, default=None): + if key not in self: + self[key] = default + return self[key] diff --git a/backend/open_webui/apps/webui/utils.py b/backend/open_webui/apps/webui/utils.py index 8bf48e400..969d5622c 100644 --- a/backend/open_webui/apps/webui/utils.py +++ b/backend/open_webui/apps/webui/utils.py @@ -87,7 +87,7 @@ def load_toolkit_module_by_id(toolkit_id, content=None): # Create a temporary file and use it to define `__file__` so # that it works as expected from the module's perspective. temp_file = tempfile.NamedTemporaryFile(delete=False) - + temp_file.close() try: with open(temp_file.name, "w", encoding="utf-8") as f: f.write(content) @@ -131,6 +131,7 @@ def load_function_module_by_id(function_id, content=None): # Create a temporary file and use it to define `__file__` so # that it works as expected from the module's perspective. temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.close() try: with open(temp_file.name, "w", encoding="utf-8") as f: f.write(content) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 504eeea54..4536f91fd 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -84,6 +84,7 @@ log_sources = [ "OPENAI", "RAG", "WEBHOOK", + "SOCKET", ] SRC_LOG_LEVELS = {} diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 64b7f2153..2ded77f2f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2329,10 +2329,11 @@ async def get_manifest_json(): return { "name": WEBUI_NAME, "short_name": WEBUI_NAME, + "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.", "start_url": "/", "display": "standalone", "background_color": "#343541", - "orientation": "portrait-primary", + "orientation": "any", "icons": [ { "src": "/static/logo.png", diff --git a/backend/requirements.txt b/backend/requirements.txt index cfe79f3ef..2554bb5f8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -69,7 +69,7 @@ rank-bm25==0.2.2 faster-whisper==1.0.3 PyJWT[crypto]==2.9.0 -authlib==1.3.1 +authlib==1.3.2 black==24.8.0 langfuse==2.44.0 diff --git a/package-lock.json b/package-lock.json index 754c89c95..bd975a9e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.3.23", + "version": "0.3.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.3.23", + "version": "0.3.24", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", @@ -1167,9 +1167,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -1179,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -1191,9 +1191,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -1203,9 +1203,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -1215,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -1227,9 +1227,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -1239,9 +1239,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -1251,9 +1251,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -1263,9 +1263,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -1275,9 +1275,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -1287,9 +1287,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -1299,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -1311,9 +1311,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -1323,9 +1323,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -1335,9 +1335,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -1347,9 +1347,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -7906,9 +7906,9 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dependencies": { "@types/estree": "1.0.5" }, @@ -7920,22 +7920,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -8724,11 +8724,11 @@ } }, "node_modules/svelte-sonner": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz", - "integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==", + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz", + "integrity": "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==", "peerDependencies": { - "svelte": ">=3 <5" + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, "node_modules/svelte/node_modules/estree-walker": { diff --git a/package.json b/package.json index 8b314654e..dddd0b0b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.3.23", + "version": "0.3.24", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 06aca67cf..b2558e4d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "faster-whisper==1.0.3", "PyJWT[crypto]==2.9.0", - "authlib==1.3.1", + "authlib==1.3.2", "black==24.8.0", "langfuse==2.44.0", diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index b6a90a953..4e521f789 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -5,6 +5,8 @@ import { PaneGroup, Pane, PaneResizer } from 'paneforge'; import { getContext, onDestroy, onMount, tick } from 'svelte'; + const i18n: Writable = getContext('i18n'); + import { goto } from '$app/navigation'; import { page } from '$app/stores'; @@ -67,11 +69,9 @@ import Navbar from '$lib/components/layout/Navbar.svelte'; import ChatControls from './ChatControls.svelte'; import EventConfirmDialog from '../common/ConfirmDialog.svelte'; - import EllipsisVertical from '../icons/EllipsisVertical.svelte'; - - const i18n: Writable = getContext('i18n'); export let chatIdProp = ''; + let loaded = false; const eventTarget = new EventTarget(); let controlPane; @@ -89,11 +89,10 @@ let eventConfirmationInputValue = ''; let eventCallback = null; - let showModelSelector = true; + let chatIdUnsubscriber: Unsubscriber | undefined; let selectedModels = ['']; let atSelectedModel: Model | undefined; - let selectedModelIds = []; $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels; @@ -104,35 +103,17 @@ let tags = []; let title = ''; - let prompt = ''; - - let chatFiles = []; - let files = []; - let messages = []; let history = { messages: {}, currentId: null }; + // Chat Input + let prompt = ''; + let chatFiles = []; + let files = []; let params = {}; - let chatIdUnsubscriber: Unsubscriber | undefined; - - $: if (history.currentId !== null) { - let _messages = []; - let currentMessage = history.messages[history.currentId]; - while (currentMessage) { - _messages.unshift({ ...currentMessage }); - currentMessage = - currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null; - } - - // This is most likely causing the performance issue - messages = _messages; - } else { - messages = []; - } - $: if (chatIdProp) { (async () => { console.log(chatIdProp); @@ -150,6 +131,7 @@ } const showMessage = async (message) => { + const _chatId = JSON.parse(JSON.stringify($chatId)); let _messageId = JSON.parse(JSON.stringify(message.id)); let messageChildrenIds = history.messages[_messageId].childrenIds; @@ -169,6 +151,9 @@ if (messageElement) { messageElement.scrollIntoView({ behavior: 'smooth' }); } + + await tick(); + saveChatHandler(_chatId); }; const chatEventHandler = async (event, cb) => { @@ -226,7 +211,7 @@ console.log('Unknown message type', data); } - messages = messages; + history.messages[event.message_id] = message; } }; @@ -308,6 +293,9 @@ showOverview.set(false); } }); + + const chatInput = document.getElementById('chat-textarea'); + chatInput?.focus(); }); onDestroy(() => { @@ -329,7 +317,6 @@ autoScroll = true; title = ''; - messages = []; history = { messages: {}, currentId: null @@ -340,6 +327,8 @@ if ($page.url.searchParams.get('models')) { selectedModels = $page.url.searchParams.get('models')?.split(','); + } else if ($page.url.searchParams.get('model')) { + selectedModels = $page.url.searchParams.get('model')?.split(','); } else if ($settings?.models) { selectedModels = $settings?.models; } else if ($config?.default_models) { @@ -424,8 +413,8 @@ autoScroll = true; await tick(); - if (messages.length > 0) { - history.messages[messages.at(-1).id].done = true; + if (history.currentId) { + history.messages[history.currentId].done = true; } await tick(); @@ -444,8 +433,12 @@ }; const createMessagesList = (responseMessageId) => { + if (responseMessageId === null) { + return []; + } + const message = history.messages[responseMessageId]; - if (message.parentId) { + if (message?.parentId) { return [...createMessagesList(message.parentId), message]; } else { return [message]; @@ -506,6 +499,8 @@ }; const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => { + const messages = createMessagesList(responseMessageId); + const res = await chatAction(localStorage.token, actionId, { model: modelId, messages: messages.map((m) => ({ @@ -564,6 +559,66 @@ }, 1000); }; + const createMessagePair = async (userPrompt) => { + prompt = ''; + if (selectedModels.length === 0) { + toast.error($i18n.t('Model not selected')); + } else { + const modelId = selectedModels[0]; + const model = $models.filter((m) => m.id === modelId).at(0); + + const messages = createMessagesList(history.currentId); + const parentMessage = messages.length !== 0 ? messages.at(-1) : null; + + const userMessageId = uuidv4(); + const responseMessageId = uuidv4(); + + const userMessage = { + id: userMessageId, + parentId: parentMessage ? parentMessage.id : null, + childrenIds: [responseMessageId], + role: 'user', + content: userPrompt ? userPrompt : `[PROMPT] ${userMessageId}`, + timestamp: Math.floor(Date.now() / 1000) + }; + + const responseMessage = { + id: responseMessageId, + parentId: userMessageId, + childrenIds: [], + role: 'assistant', + content: `[RESPONSE] ${responseMessageId}`, + done: true, + + model: modelId, + modelName: model.name ?? model.id, + modelIdx: 0, + timestamp: Math.floor(Date.now() / 1000) + }; + + if (parentMessage) { + parentMessage.childrenIds.push(userMessageId); + history.messages[parentMessage.id] = parentMessage; + } + history.messages[userMessageId] = userMessage; + history.messages[responseMessageId] = responseMessage; + + history.currentId = responseMessageId; + + await tick(); + + if (autoScroll) { + scrollToBottom(); + } + + if (messages.length === 0) { + await initChatHandler(); + } else { + await saveChatHandler($chatId); + } + } + }; + ////////////////////////// // Chat functions ////////////////////////// @@ -571,6 +626,7 @@ const submitPrompt = async (userPrompt, { _raw = false } = {}) => { let _responses = []; console.log('submitPrompt', $chatId); + const messages = createMessagesList(history.currentId); selectedModels = selectedModels.map((modelId) => $models.map((m) => m.id).includes(modelId) ? modelId : '' @@ -664,8 +720,16 @@ parentId: string, { modelId = null, modelIdx = null, newChat = false } = {} ) => { - let _responses: string[] = []; + // Create new chat if newChat is true and first user message + if ( + newChat && + history.messages[history.currentId].parentId === null && + history.messages[history.currentId].role === 'user' + ) { + await initChatHandler(); + } + let _responses: string[] = []; // If modelId is provided, use it, else use selected model let selectedModelIds = modelId ? [modelId] @@ -710,38 +774,14 @@ } await tick(); - // Create new chat if only one message in messages - if (newChat && messages.length == 2) { - if (!$temporaryChatEnabled) { - chat = await createNewChat(localStorage.token, { - id: $chatId, - title: $i18n.t('New Chat'), - models: selectedModels, - system: $settings.system ?? undefined, - params: params, - messages: messages, - history: history, - tags: [], - timestamp: Date.now() - }); - - currentChatPage.set(1); - await chats.set(await getChatList(localStorage.token, $currentChatPage)); - await chatId.set(chat.id); - } else { - await chatId.set('local'); - } - await tick(); - } - const _chatId = JSON.parse(JSON.stringify($chatId)); - await Promise.all( selectedModelIds.map(async (modelId, _modelIdx) => { console.log('modelId', modelId); const model = $models.filter((m) => m.id === modelId).at(0); if (model) { + const messages = createMessagesList(parentId); // If there are image files, check if model is vision capable const hasImages = messages.some((message) => message.files?.some((file) => file.type === 'image') @@ -840,7 +880,7 @@ }` } : undefined, - ...messages + ...createMessagesList(responseMessageId) ] .filter((message) => message?.content?.trim()) .map((message) => { @@ -891,7 +931,7 @@ } ]; files.push(...model.info.meta.knowledge); - messages = messages; // Trigger Svelte update + history.messages[responseMessageId] = responseMessage; } files.push( ...(userMessage?.files ?? []).filter((item) => @@ -912,7 +952,11 @@ await tick(); - const stream = $settings?.streamResponse ?? true; + const stream = + model?.info?.params?.stream_response ?? + $settings?.params?.stream_response ?? + params?.stream_response ?? + true; const [res, controller] = await generateChatCompletion(localStorage.token, { stream: stream, model: model.id, @@ -965,7 +1009,7 @@ const { value, done } = await reader.read(); if (done || stopResponseFlag || _chatId !== $chatId) { responseMessage.done = true; - messages = messages; + history.messages[responseMessageId] = responseMessage; if (stopResponseFlag) { controller.abort('User: Stop Response'); @@ -1032,7 +1076,7 @@ ); } - messages = messages; + history.messages[responseMessageId] = responseMessage; } } else { responseMessage.done = true; @@ -1055,7 +1099,8 @@ eval_count: data.eval_count, eval_duration: data.eval_duration }; - messages = messages; + + history.messages[responseMessageId] = responseMessage; if ($settings.notificationEnabled && !document.hasFocus()) { const notification = new Notification(`${model.id}`, { @@ -1124,7 +1169,7 @@ ); } - messages = messages; + history.messages[responseMessageId] = responseMessage; } await saveChatHandler(_chatId); @@ -1157,7 +1202,8 @@ scrollToBottom(); } - if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) { + const messages = createMessagesList(responseMessageId); + if (messages.length == 2 && messages.at(-1).content !== '' && selectedModels[0] === model.id) { window.history.replaceState(history.state, '', `/c/${_chatId}`); const _title = await generateChatTitle(userPrompt); await setChatTitle(_chatId, _title); @@ -1185,7 +1231,7 @@ } ]; files.push(...model.info.meta.knowledge); - messages = messages; // Trigger Svelte update + history.messages[responseMessageId] = responseMessage; } files.push( ...(userMessage?.files ?? []).filter((item) => @@ -1206,7 +1252,12 @@ await tick(); try { - const stream = $settings?.streamResponse ?? true; + const stream = + model?.info?.params?.stream_response ?? + $settings?.params?.stream_response ?? + params?.stream_response ?? + true; + const [res, controller] = await generateOpenAIChatCompletion( localStorage.token, { @@ -1236,7 +1287,7 @@ }` } : undefined, - ...messages + ...createMessagesList(responseMessageId) ] .filter((message) => message?.content?.trim()) .map((message, idx, arr) => ({ @@ -1314,7 +1365,7 @@ } if (done || stopResponseFlag || _chatId !== $chatId) { responseMessage.done = true; - messages = messages; + history.messages[responseMessageId] = responseMessage; if (stopResponseFlag) { controller.abort('User: Stop Response'); @@ -1369,7 +1420,7 @@ ); } - messages = messages; + history.messages[responseMessageId] = responseMessage; } if (autoScroll) { @@ -1410,7 +1461,7 @@ await saveChatHandler(_chatId); - messages = messages; + history.messages[responseMessageId] = responseMessage; stopResponseFlag = false; await tick(); @@ -1441,9 +1492,9 @@ scrollToBottom(); } + const messages = createMessagesList(responseMessageId); if (messages.length == 2 && selectedModels[0] === model.id) { window.history.replaceState(history.state, '', `/c/${_chatId}`); - const _title = await generateChatTitle(userPrompt); await setChatTitle(_chatId, _title); } @@ -1493,7 +1544,7 @@ ); } - messages = messages; + history.messages[responseMessage.id] = responseMessage; }; const stopResponse = () => { @@ -1504,7 +1555,7 @@ const regenerateResponse = async (message) => { console.log('regenerateResponse'); - if (messages.length != 0) { + if (history.currentId) { let userMessage = history.messages[message.parentId]; let userPrompt = userMessage.content; @@ -1522,11 +1573,11 @@ } }; - const continueGeneration = async () => { - console.log('continueGeneration'); + const continueResponse = async () => { + console.log('continueResponse'); const _chatId = JSON.parse(JSON.stringify($chatId)); - if (messages.length != 0 && messages.at(-1).done == true) { + if (history.currentId && history.messages[history.currentId].done == true) { const responseMessage = history.messages[history.currentId]; responseMessage.done = false; await tick(); @@ -1554,6 +1605,53 @@ } }; + const mergeResponses = async (messageId, responses, _chatId) => { + console.log('mergeResponses', messageId, responses); + const message = history.messages[messageId]; + const mergedResponse = { + status: true, + content: '' + }; + message.merged = mergedResponse; + history.messages[messageId] = message; + + try { + const [res, controller] = await generateMoACompletion( + localStorage.token, + message.model, + history.messages[message.parentId].content, + responses + ); + + if (res && res.ok && res.body) { + const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks); + for await (const update of textStream) { + const { value, done, citations, error, usage } = update; + if (error || done) { + break; + } + + if (mergedResponse.content == '' && value == '\n') { + continue; + } else { + mergedResponse.content += value; + history.messages[messageId] = message; + } + + if (autoScroll) { + scrollToBottom(); + } + } + + await saveChatHandler(_chatId); + } else { + console.error(res); + } + } catch (e) { + console.error(e); + } + }; + const generateChatTitle = async (userPrompt) => { if ($settings?.title?.auto ?? true) { const title = await generateTitle( @@ -1596,7 +1694,7 @@ description: $i18n.t('Generating search query') } ]; - messages = messages; + history.messages[responseMessageId] = responseMessage; const prompt = userMessage.content; let searchQuery = await generateSearchQuery( @@ -1616,7 +1714,7 @@ action: 'web_search', description: $i18n.t('No search query generated') }); - messages = messages; + history.messages[responseMessageId] = responseMessage; return; } @@ -1625,7 +1723,7 @@ action: 'web_search', description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery }) }); - messages = messages; + history.messages[responseMessageId] = responseMessage; const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => { console.log(error); @@ -1653,8 +1751,7 @@ type: 'web_search_results', urls: results.filenames }); - - messages = messages; + history.messages[responseMessageId] = responseMessage; } else { responseMessage.statusHistory.push({ done: true, @@ -1662,7 +1759,7 @@ action: 'web_search', description: 'No search results found' }); - messages = messages; + history.messages[responseMessageId] = responseMessage; } }; @@ -1672,13 +1769,34 @@ }); }; + const initChatHandler = async () => { + if (!$temporaryChatEnabled) { + chat = await createNewChat(localStorage.token, { + id: $chatId, + title: $i18n.t('New Chat'), + models: selectedModels, + system: $settings.system ?? undefined, + params: params, + history: history, + tags: [], + timestamp: Date.now() + }); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); + await chatId.set(chat.id); + } else { + await chatId.set('local'); + } + await tick(); + }; + const saveChatHandler = async (_chatId) => { if ($chatId == _chatId) { if (!$temporaryChatEnabled) { chat = await updateChatById(localStorage.token, _chatId, { - messages: messages, - history: history, models: selectedModels, + history: history, params: params, files: chatFiles }); @@ -1688,52 +1806,6 @@ } } }; - const mergeResponses = async (messageId, responses, _chatId) => { - console.log('mergeResponses', messageId, responses); - const message = history.messages[messageId]; - const mergedResponse = { - status: true, - content: '' - }; - message.merged = mergedResponse; - messages = messages; - - try { - const [res, controller] = await generateMoACompletion( - localStorage.token, - message.model, - history.messages[message.parentId].content, - responses - ); - - if (res && res.ok && res.body) { - const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks); - for await (const update of textStream) { - const { value, done, citations, error, usage } = update; - if (error || done) { - break; - } - - if (mergedResponse.content == '' && value == '\n') { - continue; - } else { - mergedResponse.content += value; - messages = messages; - } - - if (autoScroll) { - scrollToBottom(); - } - } - - await saveChatHandler(_chatId); - } else { - console.error(res); - } - } catch (e) { - console.error(e); - } - }; @@ -1784,18 +1856,11 @@ /> {/if} - 0} - {chat} - {initNewChat} - /> + - {#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1} + {#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner} @@ -1834,31 +1899,31 @@
0} + {selectedModels} {sendPrompt} - {continueGeneration} + {showMessage} + {continueResponse} {regenerateResponse} {mergeResponses} {chatActionHandler} - {showMessage} + bottomPadding={files.length > 0} />
{ const model = $models.find((m) => m.id === e); if (model?.info?.meta?.toolIds ?? false) { @@ -1867,10 +1932,9 @@ return a; }, [])} transparentBackground={$settings?.backgroundImageUrl ?? false} - {selectedModels} - {messages} {submitPrompt} {stopResponse} + {createMessagePair} on:call={async () => { await showControls.set(true); }} @@ -1880,6 +1944,13 @@ { const model = $models.find((m) => m.id === e); if (model) { @@ -1887,16 +1958,9 @@ } return a; }, [])} - bind:history - bind:chatFiles - bind:params - bind:files - bind:pane={controlPane} {submitPrompt} {stopResponse} {showMessage} - modelId={selectedModelIds?.at(0) ?? null} - chatId={$chatId} {eventTarget} /> diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index ef21efa6e..9cc44ce38 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -115,9 +115,9 @@ {/if} {:else} - +
- +
{ - console.log(size); if (size === 0) { showControls.set(false); } else { @@ -164,6 +163,12 @@ { + if (e.detail.node.data.message.favorite) { + history.messages[e.detail.node.data.message.id].favorite = true; + } else { + history.messages[e.detail.node.data.message.id].favorite = null; + } + showMessage(e.detail.node.data.message); }} on:close={() => { diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index ea4400bbc..ea6b0aec8 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -41,6 +41,7 @@ export let transparentBackground = false; export let submitPrompt: Function; + export let createMessagePair: Function; export let stopResponse: Function; export let autoScroll = false; @@ -61,15 +62,14 @@ let user = null; let chatInputPlaceholder = ''; - export let files = []; + export let history; + export let prompt = ''; + export let files = []; export let availableToolIds = []; export let selectedToolIds = []; export let webSearchEnabled = false; - export let prompt = ''; - export let messages = []; - let visionCapableModels = []; $: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter( (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true @@ -107,7 +107,7 @@ files = [...files, fileItem]; // Check if the file is an audio file and transcribe/convert it to text file - if (['audio/mpeg', 'audio/wav', 'audio/ogg'].includes(file['type'])) { + if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) { const res = await transcribeAudio(localStorage.token, file).catch((error) => { toast.error(error); return null; @@ -272,7 +272,7 @@
- {#if autoScroll === false && messages.length > 0} + {#if autoScroll === false && history?.currentId}
@@ -555,6 +555,12 @@ const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac const commandsContainerElement = document.getElementById('commands-container'); + // Command/Ctrl + Shift + Enter to submit a message pair + if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) { + e.preventDefault(); + createMessagePair(prompt); + } + // Check if Ctrl + R is pressed if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') { e.preventDefault(); @@ -692,7 +698,7 @@ />
- {#if messages.length == 0 || messages.at(-1).done == true} + {#if !history?.currentId || history.messages[history.currentId]?.done == true}
- {#if messages.length == 0 || messages.at(-1).done == true} + {#if !history.currentId || history.messages[history.currentId]?.done == true} {#if prompt === ''}
diff --git a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte index 9fd48c749..8e8052575 100644 --- a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte @@ -1,6 +1,14 @@
- {#if messages.length == 0} + {#if Object.keys(history?.messages ?? {}).length == 0} { @@ -327,115 +348,55 @@ {:else}
{#key chatId} - {#each messages as message, messageIdx (message.id)} -
-
+ {#if messages.at(0)?.parentId !== null} + { + console.log('visible'); + if (!messagesLoading) { + loadMoreMessages(); + } + }} > - {#if message.role === 'user'} - deleteMessageHandler(message.id)} - {user} - {readOnly} - {message} - isFirstMessage={messageIdx === 0} - siblings={message.parentId !== null - ? (history.messages[message.parentId]?.childrenIds ?? []) - : (Object.values(history.messages) - .filter((message) => message.parentId === null) - .map((message) => message.id) ?? [])} - {confirmEditMessage} - {showPreviousMessage} - {showNextMessage} - copyToClipboard={copyToClipboardWithToast} - /> - {:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1} - {#key message.id} - { - console.log('action', e); - if (typeof e.detail === 'string') { - await chatActionHandler(chatId, e.detail, message.model, message.id); - } else { - const { id, event } = e.detail; - await chatActionHandler(chatId, id, message.model, message.id, event); - } - }} - on:save={async (e) => { - console.log('save', e); - - const message = e.detail; - history.messages[message.id] = message; - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); - }} - /> - {/key} - {:else} - {#key message.parentId} - { - console.log('action', e); - if (typeof e.detail === 'string') { - await chatActionHandler(chatId, e.detail, message.model, message.id); - } else { - const { id, event } = e.detail; - await chatActionHandler(chatId, id, message.model, message.id, event); - } - }} - on:change={async () => { - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); - - if (autoScroll) { - const element = document.getElementById('messages-container'); - autoScroll = - element.scrollHeight - element.scrollTop <= element.clientHeight + 50; - setTimeout(() => { - scrollToBottom(); - }, 100); - } - }} - /> - {/key} - {/if} -
-
- {/each} +
+ +
Loading...
+
+ + {/if} + {#each messages as message, messageIdx (message.id)} + { + if (autoScroll) { + const element = document.getElementById('messages-container'); + autoScroll = + element.scrollHeight - element.scrollTop <= element.clientHeight + 50; + setTimeout(() => { + scrollToBottom(); + }, 100); + } + }} + /> + {/each} +
+
{#if bottomPadding}
{/if} diff --git a/src/lib/components/chat/Messages/CitationsModal.svelte b/src/lib/components/chat/Messages/CitationsModal.svelte index ecd1088a9..923fdea54 100644 --- a/src/lib/components/chat/Messages/CitationsModal.svelte +++ b/src/lib/components/chat/Messages/CitationsModal.svelte @@ -1,6 +1,7 @@ -{#each tokens as token, tokenIdx} +{#each tokens as token, tokenIdx (tokenIdx)} {#if token.type === 'hr'}
{:else if token.type === 'heading'} diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte new file mode 100644 index 000000000..3b07ef3db --- /dev/null +++ b/src/lib/components/chat/Messages/Message.svelte @@ -0,0 +1,166 @@ + + +
+ {#if history.messages[messageId]} + {#if history.messages[messageId].role === 'user'} + message.parentId === null) + .map((message) => message.id) ?? [])} + {showPreviousMessage} + {showNextMessage} + {editMessage} + on:delete={() => deleteMessage(messageId)} + {readOnly} + /> + {:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} + { + console.log('action', e); + const message = history.messages[messageId]; + if (typeof e.detail === 'string') { + await chatActionHandler(chatId, e.detail, message.model, message.id); + } else { + const { id, event } = e.detail; + await chatActionHandler(chatId, id, message.model, message.id, event); + } + }} + on:update={async (e) => { + console.log('update', e); + updateChatHistory(); + }} + on:save={async (e) => { + console.log('save', e); + + const message = e.detail; + if (message) { + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + history: history + }); + } else { + await updateChatById(localStorage.token, chatId, { + history: history + }); + } + }} + {readOnly} + /> + {:else} + { + console.log('action', e); + const message = history.messages[messageId]; + if (typeof e.detail === 'string') { + await chatActionHandler(chatId, e.detail, message.model, message.id); + } else { + const { id, event } = e.detail; + await chatActionHandler(chatId, id, message.model, message.id, event); + } + }} + on:update={async (e) => { + console.log('update', e); + updateChatHistory(); + }} + on:save={async (e) => { + console.log('save', e); + + const message = e.detail; + if (message) { + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + history: history + }); + } else { + await updateChatById(localStorage.token, chatId, { + history: history + }); + } + }} + on:change={async () => { + await tick(); + await updateChatById(localStorage.token, chatId, { + history: history + }); + + dispatch('scroll'); + }} + {readOnly} + /> + {/if} + {/if} +
diff --git a/src/lib/components/chat/Messages/MultiResponseMessages.svelte b/src/lib/components/chat/Messages/MultiResponseMessages.svelte index b86e579a5..7e1e7d30f 100644 --- a/src/lib/components/chat/Messages/MultiResponseMessages.svelte +++ b/src/lib/components/chat/Messages/MultiResponseMessages.svelte @@ -20,40 +20,39 @@ const i18n = getContext('i18n'); export let chatId; - export let history; - export let messages = []; - export let messageIdx; + export let messageId; - export let parentMessage; export let isLastMessage; - export let readOnly = false; - export let updateChatMessages: Function; - export let confirmEditResponseMessage: Function; + export let editMessage: Function; export let rateMessage: Function; - export let copyToClipboard: Function; - export let continueGeneration: Function; - export let mergeResponses: Function; + export let continueResponse: Function; export let regenerateResponse: Function; + export let mergeResponses: Function; const dispatch = createEventDispatcher(); let currentMessageId; - let groupedMessages = {}; - let groupedMessagesIdx = {}; + let parentMessage; + let groupedMessageIds = {}; + let groupedMessageIdsIdx = {}; - $: if (parentMessage) { - initHandler(); + let message = JSON.parse(JSON.stringify(history.messages[messageId])); + $: if (history.messages) { + if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) { + message = JSON.parse(JSON.stringify(history.messages[messageId])); + } } const showPreviousMessage = (modelIdx) => { - groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1); - let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id; + groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1); + let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]; console.log(messageId); + let messageChildrenIds = history.messages[messageId].childrenIds; while (messageChildrenIds.length !== 0) { @@ -66,12 +65,12 @@ }; const showNextMessage = (modelIdx) => { - groupedMessagesIdx[modelIdx] = Math.min( - groupedMessages[modelIdx].messages.length - 1, - groupedMessagesIdx[modelIdx] + 1 + groupedMessageIdsIdx[modelIdx] = Math.min( + groupedMessageIds[modelIdx].messageIds.length - 1, + groupedMessageIdsIdx[modelIdx] + 1 ); - let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id; + let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]; console.log(messageId); let messageChildrenIds = history.messages[messageId].childrenIds; @@ -86,33 +85,43 @@ }; const initHandler = async () => { + console.log('multiresponse:initHandler'); await tick(); - currentMessageId = messages[messageIdx].id; - groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => { + currentMessageId = messageId; + parentMessage = history.messages[messageId].parentId + ? history.messages[history.messages[messageId].parentId] + : null; + + groupedMessageIds = parentMessage?.models.reduce((a, model, modelIdx) => { // Find all messages that are children of the parent message and have the same model - let modelMessages = parentMessage?.childrenIds + let modelMessageIds = parentMessage?.childrenIds .map((id) => history.messages[id]) - .filter((m) => m?.modelIdx === modelIdx); + .filter((m) => m?.modelIdx === modelIdx) + .map((m) => m.id); - if (modelMessages.length === 0) { - modelMessages = parentMessage?.childrenIds + // Legacy support for messages that don't have a modelIdx + // Find all messages that are children of the parent message and have the same model + if (modelMessageIds.length === 0) { + let modelMessages = parentMessage?.childrenIds .map((id) => history.messages[id]) .filter((m) => m?.model === model); modelMessages.forEach((m) => { m.modelIdx = modelIdx; }); + + modelMessageIds = modelMessages.map((m) => m.id); } return { ...a, - [modelIdx]: { messages: modelMessages } + [modelIdx]: { messageIds: modelMessageIds } }; }, {}); - groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => { - const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId); + groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => { + const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId); if (idx !== -1) { return { ...a, @@ -121,39 +130,53 @@ } else { return { ...a, - [modelIdx]: 0 + [modelIdx]: groupedMessageIds[modelIdx].messageIds.length - 1 }; } }, {}); + + console.log(groupedMessageIds, groupedMessageIdsIdx); + + await tick(); }; const mergeResponsesHandler = async () => { - const responses = Object.keys(groupedMessages).map((modelIdx) => { - const { messages } = groupedMessages[modelIdx]; - return messages[groupedMessagesIdx[modelIdx]].content; + const responses = Object.keys(groupedMessageIds).map((modelIdx) => { + const { messageIds } = groupedMessageIds[modelIdx]; + const messageId = messageIds[groupedMessageIdsIdx[modelIdx]]; + + return history.messages[messageId].content; }); - mergeResponses(currentMessageId, responses, chatId); + mergeResponses(messageId, responses, chatId); }; onMount(async () => { - initHandler(); + await initHandler(); + await tick(); + + const messageElement = document.getElementById(`message-${messageId}`); + console.log(messageElement); + if (messageElement) { + messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } }); -
-
- {#key currentMessageId} - {#each Object.keys(groupedMessages) as modelIdx} - {#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0} +{#if parentMessage} +
+
+ {#each Object.keys(groupedMessageIds) as modelIdx} + {#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0} - {@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]} + {@const _messageId = + groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
{ - if (currentMessageId != message.id) { - currentMessageId = message.id; - let messageId = message.id; - console.log(messageId); - // - let messageChildrenIds = history.messages[messageId].childrenIds; + if (messageId != _messageId) { + let messageChildrenIds = history.messages[_messageId].childrenIds; while (messageChildrenIds.length !== 0) { messageId = messageChildrenIds.at(-1); - messageChildrenIds = history.messages[messageId].childrenIds; + messageChildrenIds = history.messages[_messageId].childrenIds; } - history.currentId = messageId; + history.currentId = _messageId; dispatch('change'); } }} @@ -180,95 +199,92 @@ {#key history.currentId} {#if message} m.id)} + {history} + messageId={_messageId} isLastMessage={true} - {updateChatMessages} - {confirmEditResponseMessage} + siblings={groupedMessageIds[modelIdx].messageIds} showPreviousMessage={() => showPreviousMessage(modelIdx)} showNextMessage={() => showNextMessage(modelIdx)} - {readOnly} {rateMessage} - {copyToClipboard} - {continueGeneration} + {editMessage} + {continueResponse} regenerateResponse={async (message) => { regenerateResponse(message); await tick(); - groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1; + groupedMessageIdsIdx[modelIdx] = + groupedMessageIds[modelIdx].messageIds.length - 1; }} on:action={async (e) => { dispatch('action', e.detail); }} - on:save={async (e) => { - console.log('save', e); - - const message = e.detail; - history.messages[message.id] = message; - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); + on:update={async (e) => { + dispatch('update', e.detail); }} + on:save={async (e) => { + dispatch('save', e.detail); + }} + {readOnly} /> {/if} {/key}
{/if} {/each} - {/key} -
+
- {#if !readOnly && isLastMessage} - {#if !Object.keys(groupedMessages).find((modelIdx) => { - const { messages } = groupedMessages[modelIdx]; - return !messages[groupedMessagesIdx[modelIdx]]?.done ?? false; - })} -
-
- {#if history.messages[currentMessageId]?.merged?.status} - {@const message = history.messages[currentMessageId]?.merged} + {#if !readOnly && isLastMessage} + {#if !Object.keys(groupedMessageIds).find((modelIdx) => { + const { messageIds } = groupedMessageIds[modelIdx]; + const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]]; + return !history.messages[_messageId]?.done ?? false; + })} +
+
+ {#if history.messages[messageId]?.merged?.status} + {@const message = history.messages[messageId]?.merged} -
- - Merged Response +
+ + Merged Response - {#if message.timestamp} - - {/if} - + {#if message.timestamp} + + {/if} + -
- {#if (message?.content ?? '') === ''} - - {:else} - - {/if} +
+ {#if (message?.content ?? '') === ''} + + {:else} + + {/if} +
-
- {/if} -
+ {/if} +
-
- - - +
+ + + +
-
+ {/if} {/if} - {/if} -
+
+{/if} diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index af892bcfa..6d010bd5a 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -13,6 +13,7 @@ import { synthesizeOpenAISpeech } from '$lib/apis/audio'; import { imageGenerations } from '$lib/apis/images'; import { + copyToClipboard as _copyToClipboard, approximateToHumanReadable, extractParagraphsForAudio, extractSentencesForAudio, @@ -76,25 +77,30 @@ annotation?: { type: string; rating: number }; } - export let message: MessageType; + export let history; + export let messageId; + + let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId])); + $: if (history.messages) { + if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) { + message = JSON.parse(JSON.stringify(history.messages[messageId])); + } + } + export let siblings; - export let isLastMessage = true; - - export let readOnly = false; - - export let updateChatMessages: Function; - export let confirmEditResponseMessage: Function; - export let saveNewResponseMessage: Function = () => {}; - export let showPreviousMessage: Function; export let showNextMessage: Function; + + export let editMessage: Function; export let rateMessage: Function; - export let copyToClipboard: Function; - export let continueGeneration: Function; + export let continueResponse: Function; export let regenerateResponse: Function; + export let isLastMessage = true; + export let readOnly = false; + let model = null; $: model = $models.find((m) => m.id === message.model); @@ -111,6 +117,13 @@ let showRateComment = false; + const copyToClipboard = async (text) => { + const res = await _copyToClipboard(text); + if (res) { + toast.success($i18n.t('Copying to clipboard was successful!')); + } + }; + const playAudio = (idx: number) => { return new Promise((res) => { speakingIdx = idx; @@ -260,11 +273,7 @@ }; const editMessageConfirmHandler = async () => { - if (editedContent === '') { - editedContent = ' '; - } - - confirmEditResponseMessage(message.id, editedContent); + editMessage(message.id, editedContent ? editedContent : '', false); edit = false; editedContent = ''; @@ -272,8 +281,8 @@ await tick(); }; - const saveNewMessageHandler = async () => { - saveNewResponseMessage(message, editedContent); + const saveAsCopyHandler = async () => { + editMessage(message.id, editedContent ? editedContent : ''); edit = false; editedContent = ''; @@ -313,6 +322,8 @@ } onMount(async () => { + console.log('ResponseMessage mounted'); + await tick(); }); @@ -424,7 +435,7 @@ id="save-new-message-button" class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl" on:click={() => { - saveNewMessageHandler(); + saveAsCopyHandler(); }} > {$i18n.t('Save As Copy')} @@ -909,7 +920,7 @@ ? 'visible' : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button" on:click={() => { - continueGeneration(); + continueResponse(); (model?.actions ?? []) .filter((action) => action?.__webui__ ?? false) @@ -1028,8 +1039,7 @@ bind:show={showRateComment} bind:message on:submit={(e) => { - updateChatMessages(); - + dispatch('update'); (model?.actions ?? []) .filter((action) => action?.__webui__ ?? false) .forEach((action) => { diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 351e3c0da..709ce2d51 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -1,38 +1,58 @@
diff --git a/src/lib/components/chat/Overview/Node.svelte b/src/lib/components/chat/Overview/Node.svelte index 82691c554..e00f53653 100644 --- a/src/lib/components/chat/Overview/Node.svelte +++ b/src/lib/components/chat/Overview/Node.svelte @@ -4,13 +4,14 @@ import ProfileImageBase from '../Messages/ProfileImageBase.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Heart from '$lib/components/icons/Heart.svelte'; type $$Props = NodeProps; export let data: $$Props['data'];
-
- {data?.user?.name ?? 'User'} +
+
+ {data?.user?.name ?? 'User'} +
{#if data?.message?.error} @@ -43,8 +46,24 @@ />
-
- {data?.model?.name ?? data?.message?.model ?? 'Assistant'} +
+
+ {data?.model?.name ?? data?.message?.model ?? 'Assistant'} +
+ +
{#if data?.message?.error} diff --git a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte index d1b566b63..95b27b889 100644 --- a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte +++ b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte @@ -10,6 +10,7 @@ export let params = { // Advanced + stream_response: null, // Set stream responses for this model individually seed: null, stop: null, temperature: null, @@ -42,6 +43,35 @@
+
+
+
+ {$i18n.t('Stream Chat Response')} +
+ + +
+
+
{$i18n.t('Seed')}
@@ -734,7 +764,7 @@ id="steps-range" type="range" min="-2" - max="16000" + max="131072" step="1" bind:value={params.max_tokens} class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" @@ -746,7 +776,6 @@ type="number" class=" bg-transparent text-center w-14" min="-2" - max="16000" step="1" />
diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index c308b9005..ee04deeb1 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -45,6 +45,7 @@ let params = { // Advanced + stream_response: null, seed: null, temperature: null, frequency_penalty: null, @@ -327,6 +328,7 @@ saveSettings({ system: system !== '' ? system : undefined, params: { + stream_response: params.stream_response !== null ? params.stream_response : undefined, seed: (params.seed !== null ? params.seed : undefined) ?? undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, temperature: params.temperature !== null ? params.temperature : undefined, diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index 139474c8a..58de9be4c 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -36,18 +36,11 @@ let voiceInterruption = false; let hapticFeedback = false; - let streamResponse = true; - const toggleSplitLargeChunks = async () => { splitLargeChunks = !splitLargeChunks; saveSettings({ splitLargeChunks: splitLargeChunks }); }; - const toggleStreamResponse = async () => { - streamResponse = !streamResponse; - saveSettings({ streamResponse: streamResponse }); - }; - const togglesScrollOnBranchChange = async () => { scrollOnBranchChange = !scrollOnBranchChange; saveSettings({ scrollOnBranchChange: scrollOnBranchChange }); @@ -165,7 +158,6 @@ userLocation = $settings.userLocation ?? false; hapticFeedback = $settings.hapticFeedback ?? false; - streamResponse = $settings?.streamResponse ?? true; defaultModelId = $settings?.models?.at(0) ?? ''; if ($config?.default_models) { @@ -319,28 +311,6 @@
-
-
-
- {$i18n.t('Stream Chat Response')} -
- - -
-
-
diff --git a/src/lib/components/common/Loader.svelte b/src/lib/components/common/Loader.svelte index 86c415ae4..ac7ecaf28 100644 --- a/src/lib/components/common/Loader.svelte +++ b/src/lib/components/common/Loader.svelte @@ -1,16 +1,24 @@
diff --git a/src/lib/components/common/Tooltip.svelte b/src/lib/components/common/Tooltip.svelte index a46ef7f79..230427525 100644 --- a/src/lib/components/common/Tooltip.svelte +++ b/src/lib/components/common/Tooltip.svelte @@ -13,6 +13,7 @@ export let className = 'flex'; export let theme = ''; export let allowHTML = true; + export let tippyOptions = {}; let tooltipElement; let tooltipInstance; @@ -28,7 +29,8 @@ touch: touch, ...(theme !== '' ? { theme } : { theme: 'dark' }), arrow: false, - offset: [0, 4] + offset: [0, 4], + ...tippyOptions }); } } else if (tooltipInstance && content === '') { diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index f995110dc..bc9659916 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -461,7 +461,7 @@
{/if} - {#if $pinnedChats.length > 0} + {#if !search && $pinnedChats.length > 0}
diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte index 6a2bf2294..93f13048b 100644 --- a/src/lib/components/layout/Sidebar/UserMenu.svelte +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -5,7 +5,7 @@ import { flyAndScale } from '$lib/utils/transitions'; import { goto } from '$app/navigation'; import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; - import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores'; + import { showSettings, activeUserCount, USAGE_POOL, mobile, showSidebar } from '$lib/stores'; import { fade, slide } from 'svelte/transition'; import Tooltip from '$lib/components/common/Tooltip.svelte'; @@ -41,6 +41,10 @@ on:click={async () => { await showSettings.set(true); show = false; + + if ($mobile) { + showSidebar.set(false); + } }} >
@@ -72,6 +76,10 @@ on:click={() => { dispatch('show', 'archived-chat'); show = false; + + if ($mobile) { + showSidebar.set(false); + } }} >
@@ -86,6 +94,10 @@ on:click={() => { goto('/playground'); show = false; + + if ($mobile) { + showSidebar.set(false); + } }} >
@@ -112,6 +124,10 @@ on:click={() => { goto('/admin'); show = false; + + if ($mobile) { + showSidebar.set(false); + } }} >
diff --git a/src/lib/components/layout/UpdateInfoToast.svelte b/src/lib/components/layout/UpdateInfoToast.svelte new file mode 100644 index 000000000..3b8eb3020 --- /dev/null +++ b/src/lib/components/layout/UpdateInfoToast.svelte @@ -0,0 +1,44 @@ + + +
+
+ {$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, { + LATEST_VERSION: version.latest + })} + + + {$i18n.t('Update for the latest features and improvements.')} +
+ +
+ +
+
diff --git a/src/lib/components/workspace/Documents.svelte b/src/lib/components/workspace/Documents.svelte index f1e77bddd..38f46f745 100644 --- a/src/lib/components/workspace/Documents.svelte +++ b/src/lib/components/workspace/Documents.svelte @@ -55,7 +55,7 @@ const uploadDoc = async (file, tags?: object) => { console.log(file); // Check if the file is an audio file and transcribe/convert it to text file - if (['audio/mpeg', 'audio/wav', 'audio/ogg'].includes(file['type'])) { + if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) { const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => { toast.error(error); return null; diff --git a/src/lib/components/workspace/Models.svelte b/src/lib/components/workspace/Models.svelte index 7c2b80fd4..4541b1cc9 100644 --- a/src/lib/components/workspace/Models.svelte +++ b/src/lib/components/workspace/Models.svelte @@ -1,4 +1,6 @@ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b71ac0d79..d1c30a96b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -209,4 +209,14 @@ {/if} - + diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 73be10d3f..898097272 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -155,7 +155,7 @@ bind:autoScroll bottomPadding={files.length > 0} sendPrompt={() => {}} - continueGeneration={() => {}} + continueResponse={() => {}} regenerateResponse={() => {}} />