diff --git a/server.py b/server.py index 6432f0c6..acb0e2ad 100644 --- a/server.py +++ b/server.py @@ -5,6 +5,8 @@ from flask import Response, stream_with_context from waitress import serve import webbrowser +from tkinter import filedialog, Tk +import tkinter as tk import torch @@ -272,6 +274,20 @@ def api_delete_session(): if verbose: print("->", result) return json.dumps(result) + "\n" +@app.route("/api/select_folder") +def api_select_folder(): + global api_lock, verbose + if verbose: print("/api/select_folder") + with api_lock: + root = Tk() + root.withdraw() # Hide the main window + root.attributes('-topmost', True) # Make sure dialog appears on top + folder = filedialog.askdirectory() + root.destroy() + result = {"result": "ok", "path": folder if folder else ""} + if verbose: print("->", result) + return json.dumps(result) + "\n" + @app.route("/api/remove_model", methods=['POST']) def api_remove_model(): global api_lock, verbose @@ -467,4 +483,3 @@ def api_cancel_notepad_generate(): print(f" -- Opening UI in default web browser") serve(app, host = host, port = port, threads = 8) - diff --git a/static/controls.css b/static/controls.css index 2abeeebb..e725f20e 100644 --- a/static/controls.css +++ b/static/controls.css @@ -1,6 +1,6 @@ .vflex_line { - height-min: 32px; + min-height: 32px; } .checkbox { diff --git a/static/controls.js b/static/controls.js index 1bbd7677..9b9a556b 100644 --- a/static/controls.js +++ b/static/controls.js @@ -39,6 +39,28 @@ export class LabelTextbox { if (placeholder) this.tb.placeholder = placeholder; this.tb.spellcheck = false; this.tb.value = this.data[this.data_id] ? this.data[this.data_id] : ""; + + // Create hidden span to measure text width + this.measureSpan = document.createElement("span"); + this.measureSpan.style.visibility = "hidden"; + this.measureSpan.style.position = "absolute"; + this.measureSpan.style.whiteSpace = "pre"; + // Copy font styles from input to span for accurate measurement + this.measureSpan.style.font = window.getComputedStyle(this.tb).font; + document.body.appendChild(this.measureSpan); + + // Function to update input width based on content + const updateWidth = () => { + this.measureSpan.textContent = this.tb.value || this.tb.placeholder; + const width = this.measureSpan.offsetWidth; + this.tb.style.width = (width + 20) + 'px'; // Add padding + }; + + // Update width on input + this.tb.addEventListener("input", updateWidth); + + // Initial width update + updateWidth(); this.tb.addEventListener("focus", () => { //console.log(this.data[this.data_id]); @@ -98,6 +120,10 @@ export class LabelTextbox { refresh() { let v = this.data[this.data_id] ? this.data[this.data_id] : null; this.tb.value = v; + // Update width when refreshing + this.measureSpan.textContent = this.tb.value || this.tb.placeholder; + const width = this.measureSpan.offsetWidth; + this.tb.style.width = (width + 20) + 'px'; this.refreshCB(); } @@ -249,6 +275,10 @@ export class LabelNumbox extends LabelTextbox { this.min = min; this.max = max; this.decimals = decimals; + + // Override dynamic width calculation for numeric inputs + this.measureSpan = null; // Remove the span used for width measurement + this.tb.style.width = null; // Remove any inline width style } interpret(value) { @@ -263,6 +293,11 @@ export class LabelNumbox extends LabelTextbox { refresh() { this.tb.value = this.data[this.data_id].toFixed(this.decimals); this.refreshCB(); + // Override parent's refresh method to prevent dynamic width calculation + if (this.measureSpan) { + document.body.removeChild(this.measureSpan); + this.measureSpan = null; + } } } diff --git a/static/models.css b/static/models.css index a65f7503..14357a76 100644 --- a/static/models.css +++ b/static/models.css @@ -59,7 +59,6 @@ padding: 20px; height: calc(100vh - 40px); overflow-y: auto; - flex-grow: 1; } .model-view-text { @@ -138,8 +137,42 @@ color: var(--textcolor-dim); } -.model-view-item-textbox.wide { +.folder-button { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 4px; + margin-right: 8px; + border-radius: 4px; +} + +.folder-button:hover { + background-color: var(--background-color-active); +} + +.folder-button:hover svg { + filter: brightness(1.3); +} + +.folder-button svg { + width: 24px; + height: 24px; + fill: var(--textcolor-head); +} + +.model-directory-container { + display: flex; + align-items: center; flex-grow: 1; + min-width: 0; /* Allow container to shrink below content size */ + overflow: hidden; /* Prevent overflow */ +} + +.model-view-item-textbox.wide { + flex: 0 1 auto; /* Allow textbox to shrink */ + min-width: 100px; /* Minimum width */ + max-width: calc(100% - 40px); /* Maximum width accounting for folder icon */ } .model-view-item-textbox.shortright { @@ -181,6 +214,3 @@ flex-grow: 1; justify-content: end; } - - - diff --git a/static/models.js b/static/models.js index b5c646de..a8cdc20b 100644 --- a/static/models.js +++ b/static/models.js @@ -311,8 +311,48 @@ export class ModelView { this.element.appendChild(util.newDiv(null, "model-view-text divider", "")); this.element.appendChild(util.newDiv(null, "model-view-text spacer", "")); - this.tb_model_directory = new controls.LabelTextbox("model-view-item-left", "Model directory", "model-view-item-textbox wide", "~/models/my_model/", this.modelInfo, "model_directory", null, () => { this.send() } ); - this.element.appendChild(this.tb_model_directory.element); +// Create container for model directory input and folder button +let modelDirContainer = util.newDiv(null, "model-directory-container"); +this.element.appendChild(modelDirContainer); + + // Function to normalize path separators based on platform + const normalizePath = (path) => { + const isWindows = navigator.platform.toLowerCase().includes('win'); + if (isWindows) { + return path.replace(/\//g, '\\'); + } + return path; + }; + + // Create a function to handle folder selection + const selectFolder = async () => { + try { + const response = await fetch("/api/select_folder"); + const data = await response.json(); + if (data.result === "ok" && data.path) { + // Update the model directory textbox with the selected path + this.modelInfo.model_directory = normalizePath(data.path); + this.tb_model_directory.refresh(); + this.send(); + } + } catch (err) { + console.log('Folder selection error:', err); + } + }; + +// Create model directory textbox +this.tb_model_directory = new controls.LabelTextbox("model-view-item-left", "Model directory", "model-view-item-textbox wide", "~/models/my_model/", this.modelInfo, "model_directory", null, () => { this.send() } ); +modelDirContainer.appendChild(this.tb_model_directory.label); + +// Create folder button +let folderButton = util.newDiv(null, "folder-button"); +folderButton.appendChild(util.newIcon("folder-icon")); +folderButton.addEventListener("click", () => { + selectFolder(); +}); +modelDirContainer.appendChild(folderButton); + +modelDirContainer.appendChild(this.tb_model_directory.tb); this.element_model = util.newHFlex(); this.element_model_error = util.newHFlex(); @@ -380,8 +420,39 @@ export class ModelView { //this.element_model.appendChild(util.newDiv(null, "model-view-text spacer", "")); this.element_draft_model.appendChild(util.newDiv(null, "model-view-text spacer", "")); + // Create container for draft model directory input and folder button + let draftModelDirContainer = util.newDiv(null, "model-directory-container"); + this.element_draft_model.appendChild(draftModelDirContainer); + + // Create a function to handle draft folder selection + const selectDraftFolder = async () => { + try { + const response = await fetch("/api/select_folder"); + const data = await response.json(); + if (data.result === "ok" && data.path) { + // Update the draft model directory textbox with the selected path + this.modelInfo.draft_model_directory = normalizePath(data.path); + this.tb_draft_model_directory.refresh(); + this.send(); + } + } catch (err) { + console.log('Folder selection error:', err); + } + }; + + // Create draft model directory textbox this.tb_draft_model_directory = new controls.LabelTextbox("model-view-item-left", "Draft model directory", "model-view-item-textbox wide", "~/models/my_draft_model/", this.modelInfo, "draft_model_directory", null, () => { this.send() } ); - this.element_draft_model.appendChild(this.tb_draft_model_directory.element); + draftModelDirContainer.appendChild(this.tb_draft_model_directory.label); + + // Create folder button + let draftFolderButton = util.newDiv(null, "folder-button"); + draftFolderButton.appendChild(util.newIcon("folder-icon")); + draftFolderButton.addEventListener("click", () => { + selectDraftFolder(); + }); + draftModelDirContainer.appendChild(draftFolderButton); + + draftModelDirContainer.appendChild(this.tb_draft_model_directory.tb); this.element_draft_model_s = util.newHFlex(); this.element_draft_model_error = util.newHFlex(); diff --git a/templates/svg_icons.html b/templates/svg_icons.html index 79f46b13..0bae703a 100644 --- a/templates/svg_icons.html +++ b/templates/svg_icons.html @@ -64,6 +64,14 @@ + + + + + + + +