diff --git a/python/helpers/settings.py b/python/helpers/settings.py index 045575573..06d9b312e 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -78,7 +78,7 @@ def convert_out(settings: Settings) -> dict[str, Any]: } ) - chat_model_seciton = { + chat_model_section = { "title": "Chat Model", "description": "Selection and settings for main chat model used by Agent Zero", "fields": chat_model_fields, @@ -129,13 +129,51 @@ def convert_out(settings: Settings) -> dict[str, Any]: } ) - util_model_seciton = { + util_model_section = { "title": "Utility model", "description": "Smaller, cheaper, faster model for handling utility tasks like organizing memory, preparing prompts, summarizing.", "fields": util_model_fields, } - result = {"sections": [chat_model_seciton, util_model_seciton]} + # embedding model section + embed_model_fields = [] + embed_model_fields.append( + { + "id": "embed_model_provider", + "title": "Embedding model provider", + "description": "Select provider for embedding model used by the framework", + "type": "select", + "value": settings["embed_model_provider"], + "options": [{"value": p.name, "label": p.value} for p in ModelProvider], + } + ) + embed_model_fields.append( + { + "id": "embed_model_name", + "title": "Embedding model name", + "description": "Exact name of model from selected provider", + "type": "input", + "value": settings["embed_model_name"], + } + ) + + embed_model_fields.append( + { + "id": "embed_model_kwargs", + "title": "Embedding model additional parameters", + "description": "Any other parameters supported by the model. Format is KEY=VALUE on individual lines, just like .env file.", + "type": "textarea", + "value": _dict_to_env(settings["embed_model_kwargs"]), + } + ) + + embed_model_section = { + "title": "Embedding Model", + "description": "Settings for the embedding model used by Agent Zero.", + "fields": embed_model_fields, + } + + result = {"sections": [chat_model_section, util_model_section, embed_model_section]} return result def convert_in(settings: dict[str, Any]) -> Settings: diff --git a/run_ui.py b/run_ui.py index 713084068..3b152092e 100644 --- a/run_ui.py +++ b/run_ui.py @@ -13,6 +13,7 @@ from python.helpers.files import get_abs_path from python.helpers.print_style import PrintStyle from python.helpers.dotenv import load_dotenv from python.helpers import persist_chat, settings +from werkzeug.utils import secure_filename # initialize the internal Flask server @@ -65,6 +66,61 @@ def requires_auth(f): return decorated +UPLOAD_FOLDER = os.path.join(os.getcwd(), 'work_dir', 'uploads') + +@app.route('/upload', methods=['POST']) +async def upload_file(): + if 'file' not in request.files: + return jsonify({'ok': False, 'message': 'No file part'}), 400 + + files = request.files.getlist('file') # Handle multiple files + saved_filenames = [] + + for file in files: + if file and allowed_file(file.filename): # Check file type + filename = secure_filename(file.filename) + file.save(os.path.join(UPLOAD_FOLDER, filename)) + saved_filenames.append(filename) + + return jsonify({'ok': True, 'filenames': saved_filenames}) # Return saved filenames + + +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'txt', 'pdf', 'csv', 'html', 'json', 'md'} + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + +@app.route("/import_knowledge", methods=["POST"]) +async def import_knowledge(): + if 'files[]' not in request.files: + return jsonify({'ok': False, 'message': 'No files part'}), 400 + + files = request.files.getlist('files[]') + KNOWLEDGE_FOLDER = os.path.join(os.getcwd(), 'knowledge', 'custom', 'main') + + saved_filenames = [] + + for file in files: + if file: + filename = secure_filename(file.filename) + file.save(os.path.join(KNOWLEDGE_FOLDER, filename)) + saved_filenames.append(filename) + + return jsonify({'ok': True, 'message': "Knowledge Imported", 'filenames': saved_filenames}) + + +@app.route("/work_dir", methods=["GET"]) # Correct route +async def browse_work_dir(): + work_dir = os.path.join(os.getcwd(), 'work_dir') + try: + files = [f for f in os.listdir(work_dir) if os.path.isfile(os.path.join(work_dir, f))] + return jsonify({'ok': True, 'files': files}) + except FileNotFoundError: + return jsonify({'ok': False, 'message': 'work_dir not found'}), 404 + except Exception as e: + return jsonify({'ok': False, 'message': f'Error browsing work_dir: {str(e)}'}), 500 + # handle default address, show demo html page from ./test_form.html @app.route("/", methods=["GET"]) async def test_form(): diff --git a/webui/index.css b/webui/index.css index c65bc8730..890117b3c 100644 --- a/webui/index.css +++ b/webui/index.css @@ -371,8 +371,8 @@ pre { display: flex; flex-direction: column; flex-grow: 1; - overflow-y: auto; - scroll-behavior: smooth; + overflow-y: scroll; + scroll-behavior: auto !important; /* avoid infinite scrolling! */ overflow-x: hidden; padding: var(--spacing-md) var(--spacing-md) 0; transition: all 0.3s ease; @@ -392,18 +392,15 @@ pre { } #chat-history::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); border-radius: 3px; } #chat-history::-webkit-scrollbar-thumb { border-radius: 3px; - -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); background-color: #555; transition: background-color var(--transition-speed) ease-in-out; - -webkit-transition: background-color var(--transition-speed) ease-in-out; } #chat-history::-webkit-scrollbar-thumb:hover { @@ -776,7 +773,7 @@ pre { display: flex; flex-direction: column; gap: var(--spacing-xs); - padding: var(--spacing-sm) var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) 0.8rem; background-color: var(--color-panel); } @@ -832,6 +829,7 @@ pre { width: 100%; display: flex; padding-top: var(--spacing-xs); + margin-left: 0.7rem; gap: var(--spacing-xs); white-space: pre-wrap; } @@ -889,6 +887,7 @@ pre { background-color: var(--color-primary-light); } +/* MIC BUTTON */ #microphone-button { background-color: #3270e2; } @@ -896,6 +895,9 @@ pre { #microphone-button:hover { background-color: #4382e8; } +#microphone-button.recording { + background-color: #ff4136; /* Red color for recording */ +} .chat-button svg { width: 1.5rem; @@ -920,12 +922,12 @@ pre { /* Image preview section */ .image-preview-section { display: flex; - gap: 8px; - padding: 8px; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-sm) 0.37rem var(--spacing-sm); overflow-x: auto; background-color: var(--color-input); border-radius: 8px; - margin-bottom: 8px; + margin-bottom: var(--spacing-xs); } .preview-item { @@ -934,19 +936,18 @@ pre { } .preview-item img { - width: 100px; - height: 100px; + max-height: 100px; object-fit: cover; - border-radius: 4px; - border: 1px solid var(--color-border); + border-radius: 8px; + border: 1px solid var(--color-border-light); } .remove-image { position: absolute; top: -6px; right: -6px; - width: 20px; - height: 20px; + width: 18px; + height: 18px; border-radius: 50%; background-color: var(--color-accent); color: white; @@ -1047,7 +1048,8 @@ input:checked + .slider:before { cursor: pointer; font-size: var(--font-size-normal); height: 2.525rem; - margin-left: var(--spacing-sm); + margin-left: var(--spacing-xs); + margin-right: 0.18rem; transition: background-color var(--transition-speed), transform 0.1s ease-in-out; transition: background-color var(--transition-speed), transform 0.1s ease-in-out; -webkit-transition: background-color var(--transition-speed), transform 0.1s ease-in-out; diff --git a/webui/index.html b/webui/index.html index b14877aa7..95e076d2b 100644 --- a/webui/index.html +++ b/webui/index.html @@ -213,7 +213,7 @@ @@ -236,17 +236,24 @@ - + +
- - + + +
+
+ +
+ \ No newline at end of file diff --git a/webui/index.js b/webui/index.js index 19ec79277..98bdc8a5e 100644 --- a/webui/index.js +++ b/webui/index.js @@ -12,11 +12,13 @@ const chatsSection = document.getElementById('chats-section'); const scrollbarThumb = document.querySelector('#chat-history::-webkit-scrollbar-thumb'); const progressBar = document.getElementById('progress-bar'); const autoScrollSwitch = document.getElementById('auto-scroll-switch'); - +const microphoneButton = document.getElementById('microphone-button'); let autoScroll = true; let context = ""; +let microphoneInput = null; + // Initialize the toggle button setupSidebarToggle(); @@ -95,6 +97,59 @@ chatInput.addEventListener('keydown', (e) => { sendButton.addEventListener('click', sendMessage); + +// MICROPHONE INPUT + +async function initializeMicrophoneInput() { + microphoneInput = new MicrophoneInput(updateChatInput); + await microphoneInput.initialize(); +} +function updateChatInput(text) { + chatInput.value += text + ' '; + adjustTextareaHeight(); +} +microphoneButton.addEventListener('click', () => { + if (!microphoneInput) { + initializeMicrophoneInput().then(() => { + toggleRecording(); + }); + } else { + toggleRecording(); + } +}); +function toggleRecording() { + if (microphoneInput.isRecording) { + microphoneInput.stopRecording(); + microphoneButton.classList.remove('recording'); + } else { + microphoneInput.startRecording(); + microphoneButton.classList.add('recording'); + } +} +// Some error handling for microphone input +async function requestMicrophonePermission() { + try { + await navigator.mediaDevices.getUserMedia({ audio: true }); + return true; + } catch (err) { + console.error('Error accessing microphone:', err); + toast('Microphone access denied. Please enable microphone access in your browser settings.', 'error'); + return false; + } +} +// microphoneButton click event listener modifier +microphoneButton.addEventListener('click', async () => { + const hasPermission = await requestMicrophonePermission(); + if (!hasPermission) return; + if (!microphoneInput) { + initializeMicrophoneInput().then(() => { + toggleRecording(); + }); + } else { + toggleRecording(); + } +}); + function updateUserTime() { const now = new Date(); const hours = now.getHours(); @@ -147,6 +202,94 @@ function setMessage(id, type, heading, content, temp, kvps = null) { } +async function handleFileUpload(event) { + const files = event.target.files; + const formData = new FormData(); + for (let i = 0; i < files.length; i++) { + formData.append('file', files[i]); + } + + const response = await fetch('/upload', { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + if (!data.ok) { + toast(data.message, "error"); + } else { + toast("Files uploaded: " + data.filenames.join(", "), "success"); + } +} + + +window.loadKnowledge = async function() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.txt,.pdf,.csv,.html,.json,.md'; + input.multiple = true; + + input.onchange = async () => { + const formData = new FormData(); + for (let file of input.files) { + formData.append('files[]', file); + } + + const response = await fetch('/import_knowledge', { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + if (!data.ok) { + toast(data.message, "error"); + } else { + toast("Knowledge files imported: " + data.filenames.join(", "), "success"); + } + }; + + input.click(); +} + + +const workDirModalProxy = { + isOpen: false, + files: [], + + async openModal() { // Define openModal + // Inside openModal, call the existing open method: + await this.open(); // Or directly include the fetching logic here + }, + + async open() { + const response = await sendJsonData('/work_dir'); + if (response.ok) { + this.files = response.files; + this.isOpen = true; + } else { + toast(response.message, 'error'); + } + }, + + close() { + this.isOpen = false; + } +}; + +// Make the proxy available globally +window.workDirModalProxy = workDirModalProxy; + +// Ensure correct setup for Alpine.js x-data. +window.workDirModal = function() { + return workDirModalProxy; // Returns the proxy object for the Work Dir modal +} + + +document.addEventListener('alpine:init', () => { + // Make workDirModalProxy available as an Alpine component/store + Alpine.data('workDirModal', workDirModal); +}); + function adjustTextareaHeight() { chatInput.style.height = 'auto'; @@ -563,4 +706,4 @@ async function startPolling() { _doPoll(); } -document.addEventListener("DOMContentLoaded", startPolling); +document.addEventListener("DOMContentLoaded", startPolling); \ No newline at end of file