This commit is contained in:
ChrisColeTech 2025-09-04 09:20:50 +04:00 committed by GitHub
commit bcae8a68eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 17110 additions and 614 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ user_path_config-deprecated.txt
/.coverage*
/auth.json
.DS_Store
/.venv

View File

@ -0,0 +1,23 @@
import os
import importlib
from flask import Blueprint
from flask_restx import Namespace
def register_blueprints(app, api):
"""Register all Blueprints to the Flask app automatically."""
controllers_dir = os.path.dirname(__file__)
for filename in os.listdir(controllers_dir):
if filename.endswith('_controller.py') and filename != '__init__.py':
module_name = filename[:-3] # Remove ".py"
module = importlib.import_module(
f'.{module_name}', package=__package__)
for attribute_name in dir(module):
attribute = getattr(module, attribute_name)
if isinstance(attribute, Namespace):
api.add_namespace(attribute)
if isinstance(attribute, Blueprint):
app.register_blueprint(
attribute)
print(f"Registered blueprint: {attribute_name}")

View File

@ -0,0 +1,105 @@
from flask_restx import Api, Resource, fields, Namespace
from flask import jsonify, request, make_response, Blueprint
import psutil
import GPUtil
import time
# Create a Blueprint for the gpu_usage controller
gpu_usage_bp = Blueprint('gpu_usage', __name__)
gpu_usage_api = Api(gpu_usage_bp, version='1.0', title='gpu_usage API',
description='API for managing gpu_usage')
# Define a namespace for gpu_usage
gpu_usage_ns = Namespace('gpu_usage', description='gpu usage operations')
# Define the model for a gpu
gpu_model = gpu_usage_ns.model('gpu_usage', {
'id': fields.Integer(required=True, description='The unique identifier of the gpu'),
'description': fields.String(required=True, description='Description of the gpu'),
'status': fields.String(description='Status of the gpu')
})
# Cache for system usage data
cache = {
'timestamp': 0,
'data': {
'cpu': 0,
'ram': 0,
'gpu': 0,
'vram': 0,
'hdd': 0,
'temp': 0
}
}
CACHE_DURATION = 1 # Cache duration in seconds
@gpu_usage_ns.route('/')
class GPUInfo(Resource):
def get_cache(self, current_time):
# Get CPU utilization
cpu_percent = psutil.cpu_percent(interval=0)
# Get Memory utilization
mem = psutil.virtual_memory()
mem_percent = mem.percent
# Get GPU utilization (considering only the first GPU)
gpus = GPUtil.getGPUs()
gpu_percent = gpus[0].load * 100 if gpus else 0
# Get VRAM usage (considering only the first GPU)
vram_usage = 0
if gpus:
used = gpus[0].memoryUsed
total = gpus[0].memoryTotal
vram_usage = (used / total) * 100
# Get HDD usage (assuming usage of the primary disk)
hdd = psutil.disk_usage('/')
hdd_percent = hdd.percent
# Get temperature (if available)
temperature = gpus[0].temperature
# Update the cache
cache['data'] = {
'cpu': cpu_percent,
'ram': mem_percent,
'gpu': gpu_percent,
'vram': vram_usage, # Convert bytes to MB
'hdd': hdd_percent,
'temp': temperature # Add temperature
}
cache['timestamp'] = current_time
return cache
def get(self):
if request.method == "OPTIONS": # CORS preflight
return _build_cors_preflight_response()
current_time = time.time()
# Check if the cache is still valid
if current_time - cache['timestamp'] < CACHE_DURATION:
return _corsify_actual_response(jsonify(cache['data']))
try:
self.get_cache(current_time)
return _corsify_actual_response(jsonify(cache['data']))
except Exception as e:
return _corsify_actual_response(jsonify({'error': str(e)}))
def _build_cors_preflight_response():
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "*")
response.headers.add("Access-Control-Allow-Methods", "*")
return response
def _corsify_actual_response(response):
response.headers.add("Access-Control-Allow-Origin", "*")
return response

View File

@ -0,0 +1,110 @@
import pickle
import os
from flask import Blueprint, request, jsonify, make_response
from flask_restx import Api, Resource, fields, Namespace
# Create a Blueprint for the jobs controller
jobs_bp = Blueprint('jobs', __name__)
jobs_api = Api(jobs_bp, version='1.0', title='Jobs API',
description='API for managing jobs')
# Define a namespace for jobs
jobs_ns = Namespace('jobs', description='Job operations')
# Define the model for a job
job_model = jobs_ns.model('Job', {
'id': fields.Integer(required=True, description='The unique identifier of the job'),
'description': fields.String(required=True, description='Description of the job'),
'status': fields.String(description='Status of the job')
})
# File to persist data
DATA_FILE = 'jobs.pkl'
def load_jobs():
if os.path.exists(DATA_FILE):
with open(DATA_FILE, 'rb') as file:
return pickle.load(file)
else:
# Create an empty file if it doesn't exist
with open(DATA_FILE, 'wb') as file:
pickle.dump({}, file)
return {}
def save_jobs(jobs):
with open(DATA_FILE, 'wb') as file:
pickle.dump(jobs, file)
# Load initial data
jobs_store = load_jobs()
@jobs_ns.route('/')
class JobList(Resource):
def get(self):
"""List all jobs"""
jobs_store = load_jobs()
return _corsify_actual_response(jsonify(list(jobs_store.values())))
@jobs_ns.expect(job_model)
def post(self):
"""Create a new job"""
if request.method == "OPTIONS": # CORS preflight
return _build_cors_preflight_response()
job = request.json
job_id = job['id']
if job_id in jobs_store:
return {'message': 'Job already exists'}, 400
jobs_store[job_id] = job
save_jobs(jobs_store) # Save to file
return _corsify_actual_response(jsonify(job))
@jobs_ns.route('/<int:job_id>')
class JobItem(Resource):
def get(self, job_id):
"""Get a job by ID"""
job = jobs_store.get(job_id)
if job is None:
return {'message': 'Job not found'}, 404
return _corsify_actual_response(jsonify(job))
@jobs_ns.expect(job_model)
def put(self, job_id):
"""Update a job by ID"""
if request.method == "OPTIONS": # CORS preflight
return _build_cors_preflight_response()
job = request.json
if job_id not in jobs_store:
return {'message': 'Job not found'}, 404
jobs_store[job_id] = job
save_jobs(jobs_store) # Save to file
return _corsify_actual_response(jsonify(job))
def delete(self, job_id):
"""Delete a job by ID"""
if request.method == "OPTIONS": # CORS preflight
return _build_cors_preflight_response()
if job_id not in jobs_store:
return {'message': 'Job not found'}, 404
del jobs_store[job_id]
save_jobs(jobs_store) # Save to file
return _corsify_actual_response(jsonify({'message': 'Job deleted'}))
def _build_cors_preflight_response():
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "*")
response.headers.add("Access-Control-Allow-Methods", "*")
return response
def _corsify_actual_response(response):
response.headers.add("Access-Control-Allow-Origin", "*")
return response

View File

@ -0,0 +1,71 @@
import json
import os
from flask import Blueprint, jsonify, request
from flask_restx import Api, Resource, fields, Namespace
# Create a Blueprint for the settings controller
settings_bp = Blueprint('settings', __name__)
settings_api = Api(settings_bp, version='1.0', title='Settings API',
description='API for managing settings')
# Define a namespace for settings
settings_ns = Namespace('settings', description='Settings operations')
# Define the model for settings
settings_model = settings_ns.model('Setting', {
'key': fields.String(required=True, description='The key of the setting'),
'value': fields.String(required=True, description='The value of the setting')
})
# File to persist settings data
SETTINGS_FILE = 'settings.json'
def load_settings():
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, 'r') as file:
return json.load(file)
return {}
def save_settings(settings):
with open(SETTINGS_FILE, 'w') as file:
json.dump(settings, file, indent=4)
# Load initial data
settings_store = load_settings()
@settings_ns.route('/')
class SettingsList(Resource):
def get(self):
"""List all settings"""
return jsonify({'settings': list(settings_store.values())})
@settings_ns.expect(settings_model)
def post(self):
"""Create or update a setting"""
setting = request.json
key = setting['key']
settings_store[key] = setting
save_settings(settings_store) # Save to file
return jsonify(setting)
@settings_ns.route('/<string:key>')
class SettingItem(Resource):
def get(self, key):
"""Get a setting by key"""
setting = settings_store.get(key)
if setting is None:
return {'message': 'Setting not found'}, 404
return jsonify(setting)
def delete(self, key):
"""Delete a setting by key"""
if key not in settings_store:
return {'message': 'Setting not found'}, 404
del settings_store[key]
save_settings(settings_store) # Save to file
return {'message': 'Setting deleted'}

214
api/dependency_installer.py Normal file
View File

@ -0,0 +1,214 @@
import subprocess
import sys
import os
import shutil
import zipfile
import importlib
import urllib.request
import re
re_requirement = re.compile(r"\s*([-\w]+)\s*(?:==\s*([-+.\w]+))?\s*")
python = sys.executable
default_command_live = (os.environ.get('LAUNCH_LIVE_OUTPUT') == "1")
index_url = os.environ.get('INDEX_URL', "")
modules_path = os.path.dirname(os.path.realpath(__file__))
script_path = os.path.dirname(modules_path)
def detect_python_version():
version = sys.version_info
version_str = f"{version.major}.{version.minor}"
is_embedded = hasattr(sys, '_base_executable') or (
sys.base_prefix != sys.prefix and not hasattr(sys, 'real_prefix'))
return version_str, is_embedded
def check_tkinter_installed():
version_str, is_embedded = detect_python_version()
print(f"Detected Python version: {version_str}")
print(f"Is Embedded Python: {is_embedded}")
try:
import tkinter
tkinter_installed = True
except ImportError:
tkinter_installed = False
if not tkinter_installed or (is_embedded and not tkinter_installed):
install_tkinter(version_str)
def check_GPUtil_installed():
try:
import GPUtil
import psutil
return True
except ImportError:
import_GPUtil()
return False
def check_flask_installed():
try:
import flask
import flask_restx
import flask_cors
import flask_socketio
return True
except ImportError:
import_flask()
return False
def download_and_unzip_tkinter():
url = "https://github.com/ChrisColeTech/tkinter-standalone/releases/download/1.0.0/tkinter-standalone.zip"
zip_path = "tkinter-standalone.zip"
print(f"Downloading {url}...")
urllib.request.urlretrieve(url, zip_path)
print("Unzipping tkinter-standalone.zip...")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall("tkinter-standalone")
os.remove(zip_path)
print("Download and extraction complete.")
def copy_tkinter_files(version_str):
src_folder = os.path.join("tkinter-standalone",
version_str, "python_embedded")
number_only = version_str.replace(".", "")
python_zip = f"python{number_only}"
python_zip_path = os.path.join(src_folder, f"{python_zip}.zip")
with zipfile.ZipFile(python_zip_path, 'r') as zip_ref:
zip_ref.extractall(os.path.join(src_folder, python_zip))
if not os.path.exists(src_folder):
print(f"Error: No tkinter files for Python {version_str}")
return
# Define paths
python_dir = os.path.dirname(sys.executable)
pth_filename = f"{python_zip}._pth"
pth_path_src = os.path.join(src_folder, pth_filename)
pth_path_dest = os.path.join(python_dir, pth_filename)
# Copy the .pth file from python_dir to src_folder
if os.path.exists(pth_path_dest):
shutil.copy(pth_path_dest, pth_path_src)
else:
print(f"Error: {pth_filename} not found in {python_dir}")
return
# Modify the .pth file
with open(pth_path_src, 'a') as pth_file:
pth_file.write(f'\n./{python_zip}\n')
pth_file.write('./Scripts\n')
pth_file.write('./DLLs\n')
print(f"Copying tkinter files from {src_folder} to {python_dir}...")
shutil.copytree(src_folder, python_dir, dirs_exist_ok=True)
print("Tkinter files copied successfully.")
shutil.rmtree("tkinter-standalone", ignore_errors=True)
def install_tkinter(version_str):
download_and_unzip_tkinter()
copy_tkinter_files(version_str)
import_tkinter()
def import_tkinter():
try:
tkinter = importlib.import_module("tkinter")
print("tkinter module loaded successfully.")
return tkinter
except ModuleNotFoundError as e:
print(f"Module not found: {e}")
except ImportError as e:
print("Failed to import Tkinter after installation.")
print(f"An error occurred: {e}")
except Exception as e:
print(f"An error occurred: {e}")
return None
def import_GPUtil():
run_pip(f"install GPUtil psutil", desc="GPU Utility for NVIDIA GPUs")
try:
GPUtil = importlib.import_module(
"GPUtil")
psutil = importlib.import_module(
"psutil")
return GPUtil
except ImportError:
print("Failed to import GPUtil after installation.")
return None
def import_flask():
run_pip(f"install flask flask-restx flask-cors flask_socketio", desc="Flask Rest API")
try:
flask = importlib.import_module("flask")
restx = importlib.import_module("flask-restx")
flask_socketio = importlib.import_module("flask_socketio")
return restx
except ImportError:
print("Failed to import flask after installation.")
return None
def run(command, desc=None, errdesc=None, custom_env=None, live: bool = default_command_live) -> str:
if desc is not None:
print(desc)
run_kwargs = {
"args": command,
"shell": True,
"env": os.environ if custom_env is None else custom_env,
"encoding": 'utf8',
"errors": 'ignore',
}
if not live:
run_kwargs["stdout"] = run_kwargs["stderr"] = subprocess.PIPE
result = subprocess.run(**run_kwargs)
if result.returncode != 0:
error_bits = [
f"{errdesc or 'Error running command'}.",
f"Command: {command}",
f"Error code: {result.returncode}",
]
if result.stdout:
error_bits.append(f"stdout: {result.stdout}")
if result.stderr:
error_bits.append(f"stderr: {result.stderr}")
raise RuntimeError("\n".join(error_bits))
return (result.stdout or "")
def run_pip(command, desc=None, live=default_command_live):
try:
index_url_line = f' --index-url {index_url}' if index_url != '' else ''
return run(f'"{python}" -m pip {command} --prefer-binary{index_url_line}', desc=f"Installing {desc}",
errdesc=f"Couldn't install {desc}", live=live)
except Exception as e:
print(e)
print(f'CMD Failed {desc}: {command}')
return None
check_tkinter_installed()
check_GPUtil_installed()
check_flask_installed()

18
api/gradio_helper.py Normal file
View File

@ -0,0 +1,18 @@
import gradio as gr
import os
from .http_server import *
def addResourceMonitor():
ceq = None
with gr.Row():
ceq = gr.HTML(load_page('templates/perf-monitor/index.html'))
return ceq
def load_page(filename):
"""Load an HTML file as a string and return it"""
file_path = os.path.join("web", filename)
with open(file_path, 'r') as file:
content = file.read()
return content

116
api/http_server.py Normal file
View File

@ -0,0 +1,116 @@
from .dependency_installer import *
from flask import Flask, send_from_directory, render_template
from flask_socketio import SocketIO, emit
from flask_restx import Api
import logging
import time
from .controllers import register_blueprints
import psutil
import GPUtil
import threading
# Initialize Flask app
title = f"Resource Monitor"
app = Flask(title, static_folder='web/assets', template_folder='web/templates')
app.config['CORS_HEADERS'] = 'Content-Type'
# Initialize Flask-RESTx API
api = Api(app, version='1.0', title=title, description='API for system resource monitoring')
# Register blueprints (API endpoints)
register_blueprints(app, api)
# Initialize SocketIO with the Flask app
socketio = SocketIO(app, cors_allowed_origins="*")
# Cache for system usage data
cache = {
'timestamp': 0,
'data': {
'cpu': 0,
'ram': 0,
'gpu': 0,
'vram': 0,
'hdd': 0,
'temp': 0
}
}
CACHE_DURATION = 1 # Cache duration in seconds
# Suppress the Flask development server warning
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR) # Set level to ERROR to suppress warnings
def get_cache(current_time):
# Get CPU utilization
cpu_percent = psutil.cpu_percent(interval=0)
# Get Memory utilization
mem = psutil.virtual_memory()
mem_percent = mem.percent
# Get GPU utilization (considering only the first GPU)
gpus = GPUtil.getGPUs()
gpu_percent = gpus[0].load * 100 if gpus else 0
# Get VRAM usage (considering only the first GPU)
vram_usage = 0
if gpus:
used = gpus[0].memoryUsed
total = gpus[0].memoryTotal
vram_usage = (used / total) * 100
# Get HDD usage (assuming usage of the primary disk)
hdd = psutil.disk_usage('/')
hdd_percent = hdd.percent
# Get temperature (if available)
temperature = gpus[0].temperature if gpus else 0
# Update the cache
cache['data'] = {
'cpu': cpu_percent,
'ram': mem_percent,
'gpu': gpu_percent,
'vram': vram_usage, # Convert bytes to MB
'hdd': hdd_percent,
'temp': temperature # Add temperature
}
cache['timestamp'] = current_time
@app.route('/')
def home():
return render_template('index.html')
@app.route('/<path:filename>')
def serve_static(filename):
return send_from_directory('web', filename)
@socketio.on('connect')
def handle_connect():
# Emit initial data
current_time = time.time()
get_cache(current_time)
emit('data_update', cache['data'])
@socketio.on('disconnect')
def handle_disconnect():
pass
def background_thread():
while True:
current_time = time.time()
get_cache(current_time)
socketio.emit('data_update', cache['data'])
time.sleep(.5)
def run_app():
time.sleep(1) # Sleep for a short while to let the server start
# Start the background thread for emitting data
socketio.start_background_task(target=background_thread)
# Run the Flask app with SocketIO
socketio.run(app, port=5000)
# Start Flask app in a separate thread
thread = threading.Thread(target=run_app)
thread.start()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
import gradio as gr
import os
import modules.config
import modules.html
import modules.meta_parser
from tkinter import Tk, filedialog
def process_directories(directory_paths):
if not directory_paths:
return "No directories selected."
results = []
for directory in directory_paths:
# List files in the directory
files = os.listdir(directory)
results.append(f"Contents of {directory}:\n" + "\n".join(files))
return "\n\n".join(results)
def update_visibility(x):
# Add more updates for other components
return [gr.update(visible=x), gr.update(visible=x)]
def list_to_string(filenames):
# Join the filenames list into a comma-separated string
file_list = ', '.join(filenames)
return file_list
def on_browse(data_type):
root = Tk()
root.attributes("-topmost", True)
root.withdraw()
if data_type == "Files":
filenames = filedialog.askopenfilenames()
if len(filenames) > 0:
root.destroy()
file_list = list_to_string(filenames)
return file_list
else:
filename = "Files not seleceted"
root.destroy()
return None
elif data_type == "Folder":
filename = filedialog.askdirectory()
if filename:
if os.path.isdir(filename):
root.destroy()
return str(filename)
else:
root.destroy()
return str(filename)
else:
filename = "Folder not seleceted"
root.destroy()
return None
def on_file_change(files, data_type):
if files and data_type == "Files":
return gr.update(visible=True), gr.update(), gr.update(value=True)
# If no files are selected, hide file explorer and clear input_path
if not files and data_type == "Files":
return gr.update(visible=False), gr.update(value=""), gr.update(value=False)
if data_type == "Folder":
return gr.update(visible=False), gr.update(), gr.update(value=True)
return gr.update(visible=False), gr.update(), gr.update(value=False)
def on_input_change(input, file_explorer):
if input:
# Verify with normalised version of path
input_path = os.path.normpath(os.path.realpath(input))
if os.path.isdir(os.path.realpath(input_path)):
# Return an empty list if input_path is a directory
return None, gr.update(visible=True), gr.update(value=True)
else:
# Return an empty list if input_path is empty
return None, gr.update(visible=False), gr.update(value=False)
# Initialize a dictionary to track unique file names and their paths
unique_file_paths = {}
# Process the input_path string
if input_path:
# Clean up the input path string and split it into a list of file paths
file_paths_list = input_path.strip("()").replace("'", "").split(", ")
# Extract file names and ensure uniqueness
for path in file_paths_list:
file_name = os.path.basename(path)
unique_file_paths[file_name] = path
# Process file_explorer items if provided
if file_explorer:
# Extract 'orig_name' from each file_explorer object and ensure uniqueness
for item in file_explorer:
sanitized_path = item.orig_name
file_name = os.path.basename(sanitized_path)
# Store the path, replacing any existing path with the same file name
unique_file_paths[file_name] = sanitized_path
# Convert the dictionary values back to a list of unique file paths
if len(unique_file_paths.values()) > 0:
return list(unique_file_paths.values()), gr.update(visible=False), gr.update(value=True)
else:
return None, gr.update(visible=False), gr.update(value=False)
def on_click_clear():
return None, None, gr.update(visible=False), gr.update(visible=False)
# Function to set prompts based on the selected type
def update_prompts(selected_type):
# Ensure selected_type is a valid key and exists in the dictionary
if selected_type in modules.config.default_enhance_prompts:
positive_prompt = modules.config.default_enhance_prompts[selected_type]['positive']
negative_prompt = modules.config.default_enhance_prompts[selected_type]['negative']
return positive_prompt, negative_prompt
else:
# Returning default or empty values
return "Default positive prompt", "Default negative prompt"
def on_selection_change(selected_type):
# Get prompts based on selected_type
positive_prompt, negative_prompt = update_prompts(selected_type[0])
# Return the prompts
return positive_prompt, negative_prompt

View File

@ -7,7 +7,6 @@ import args_manager
import tempfile
import modules.flags
import modules.sdxl_styles
from modules.model_loader import load_file_from_url
from modules.extra_utils import makedirs_with_log, get_files_from_folder, try_eval_env_var
from modules.flags import OutputFormat, Performance, MetadataScheme
@ -21,9 +20,11 @@ def get_config_path(key, default_value):
else:
return os.path.abspath(default_value)
wildcards_max_bfs_depth = 64
config_path = get_config_path('config_path', "./config.txt")
config_example_path = get_config_path('config_example_path', "config_modification_tutorial.txt")
config_example_path = get_config_path(
'config_example_path', "config_modification_tutorial.txt")
config_dict = {}
always_save_keys = []
visited_keys = []
@ -41,9 +42,11 @@ try:
config_dict.update(json.load(json_file))
always_save_keys = list(config_dict.keys())
except Exception as e:
print(f'Failed to load config file "{config_path}" . The reason is: {str(e)}')
print(
f'Failed to load config file "{config_path}" . The reason is: {str(e)}')
print('Please make sure that:')
print(f'1. The file "{config_path}" is a valid text file, and you have access to read it.')
print(
f'1. The file "{config_path}" is a valid text file, and you have access to read it.')
print('2. Use "\\\\" instead of "\\" when describing paths.')
print('3. There is no "," before the last "}".')
print('4. All key/value formats are correct.')
@ -56,7 +59,8 @@ def try_load_deprecated_user_path_config():
return
try:
deprecated_config_dict = json.load(open('user_path_config.txt', "r", encoding="utf-8"))
deprecated_config_dict = json.load(
open('user_path_config.txt', "r", encoding="utf-8"))
def replace_config(old_key, new_key):
if old_key in deprecated_config_dict:
@ -75,7 +79,8 @@ def try_load_deprecated_user_path_config():
replace_config('temp_outputs_path', 'path_outputs')
if deprecated_config_dict.get("default_model", None) == 'juggernautXL_version6Rundiffusion.safetensors':
os.replace('user_path_config.txt', 'user_path_config-deprecated.txt')
os.replace('user_path_config.txt',
'user_path_config-deprecated.txt')
print('Config updated successfully in silence. '
'A backup of previous config is written to "user_path_config-deprecated.txt".')
return
@ -86,7 +91,8 @@ def try_load_deprecated_user_path_config():
print('Loading using deprecated old models and deprecated old configs.')
return
else:
os.replace('user_path_config.txt', 'user_path_config-deprecated.txt')
os.replace('user_path_config.txt',
'user_path_config-deprecated.txt')
print('Config updated successfully by user. '
'A backup of previous config is written to "user_path_config-deprecated.txt".')
return
@ -98,6 +104,7 @@ def try_load_deprecated_user_path_config():
try_load_deprecated_user_path_config()
def get_presets():
preset_folder = 'presets'
presets = ['initial']
@ -107,10 +114,12 @@ def get_presets():
return presets + [f[:f.index(".json")] for f in os.listdir(preset_folder) if f.endswith('.json')]
def update_presets():
global available_presets
available_presets = get_presets()
def try_get_preset_content(preset):
if isinstance(preset, str):
preset_path = os.path.abspath(f'./presets/{preset}.json')
@ -127,18 +136,22 @@ def try_get_preset_content(preset):
print(e)
return {}
available_presets = get_presets()
preset = args_manager.args.preset
config_dict.update(try_get_preset_content(preset))
def get_path_output() -> str:
"""
Checking output path argument and overriding default path.
"""
global config_dict
path_output = get_dir_or_set_default('path_outputs', '../outputs/', make_directory=True)
path_output = get_dir_or_set_default(
'path_outputs', '../outputs/', make_directory=True)
if args_manager.args.output_path:
print(f'Overriding config value path_outputs with {args_manager.args.output_path}')
print(
f'Overriding config value path_outputs with {args_manager.args.output_path}')
config_dict['path_outputs'] = path_output = args_manager.args.output_path
return path_output
@ -172,15 +185,18 @@ def get_dir_or_set_default(key, default_value, as_array=False, make_directory=Fa
return v
if v is not None:
print(f'Failed to load config key: {json.dumps({key:v})} is invalid or does not exist; will use {json.dumps({key:default_value})} instead.')
print(
f'Failed to load config key: {json.dumps({key:v})} is invalid or does not exist; will use {json.dumps({key:default_value})} instead.')
if isinstance(default_value, list):
dp = []
for path in default_value:
abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), path))
abs_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), path))
dp.append(abs_path)
os.makedirs(abs_path, exist_ok=True)
else:
dp = os.path.abspath(os.path.join(os.path.dirname(__file__), default_value))
dp = os.path.abspath(os.path.join(
os.path.dirname(__file__), default_value))
os.makedirs(dp, exist_ok=True)
if as_array:
dp = [dp]
@ -188,18 +204,26 @@ def get_dir_or_set_default(key, default_value, as_array=False, make_directory=Fa
return dp
paths_checkpoints = get_dir_or_set_default('path_checkpoints', ['../models/checkpoints/'], True)
paths_checkpoints = get_dir_or_set_default(
'path_checkpoints', ['../models/checkpoints/'], True)
paths_loras = get_dir_or_set_default('path_loras', ['../models/loras/'], True)
path_embeddings = get_dir_or_set_default('path_embeddings', '../models/embeddings/')
path_vae_approx = get_dir_or_set_default('path_vae_approx', '../models/vae_approx/')
path_embeddings = get_dir_or_set_default(
'path_embeddings', '../models/embeddings/')
path_vae_approx = get_dir_or_set_default(
'path_vae_approx', '../models/vae_approx/')
path_vae = get_dir_or_set_default('path_vae', '../models/vae/')
path_upscale_models = get_dir_or_set_default('path_upscale_models', '../models/upscale_models/')
path_upscale_models = get_dir_or_set_default(
'path_upscale_models', '../models/upscale_models/')
path_inpaint = get_dir_or_set_default('path_inpaint', '../models/inpaint/')
path_controlnet = get_dir_or_set_default('path_controlnet', '../models/controlnet/')
path_clip_vision = get_dir_or_set_default('path_clip_vision', '../models/clip_vision/')
path_fooocus_expansion = get_dir_or_set_default('path_fooocus_expansion', '../models/prompt_expansion/fooocus_expansion')
path_controlnet = get_dir_or_set_default(
'path_controlnet', '../models/controlnet/')
path_clip_vision = get_dir_or_set_default(
'path_clip_vision', '../models/clip_vision/')
path_fooocus_expansion = get_dir_or_set_default(
'path_fooocus_expansion', '../models/prompt_expansion/fooocus_expansion')
path_wildcards = get_dir_or_set_default('path_wildcards', '../wildcards/')
path_safety_checker = get_dir_or_set_default('path_safety_checker', '../models/safety_checker/')
path_safety_checker = get_dir_or_set_default(
'path_safety_checker', '../models/safety_checker/')
path_sam = get_dir_or_set_default('path_sam', '../models/sam/')
path_outputs = get_path_output()
@ -209,7 +233,7 @@ def get_config_item_or_set_default(key, default_value, validator, disable_empty_
if key not in visited_keys:
visited_keys.append(key)
v = os.getenv(key)
if v is not None:
v = try_eval_env_var(v, expected_type)
@ -228,7 +252,8 @@ def get_config_item_or_set_default(key, default_value, validator, disable_empty_
return v
else:
if v is not None:
print(f'Failed to load config key: {json.dumps({key:v})} is invalid; will use {json.dumps({key:default_value})} instead.')
print(
f'Failed to load config key: {json.dumps({key:v})} is invalid; will use {json.dumps({key:default_value})} instead.')
config_dict[key] = default_value
return default_value
@ -274,7 +299,8 @@ default_base_model_name = default_model = get_config_item_or_set_default(
previous_default_models = get_config_item_or_set_default(
key='previous_default_models',
default_value=[],
validator=lambda x: isinstance(x, list) and all(isinstance(k, str) for k in x),
validator=lambda x: isinstance(x, list) and all(
isinstance(k, str) for k in x),
expected_type=list
)
default_refiner_model_name = default_refiner = get_config_item_or_set_default(
@ -331,15 +357,18 @@ default_loras = get_config_item_or_set_default(
]
],
validator=lambda x: isinstance(x, list) and all(
len(y) == 3 and isinstance(y[0], bool) and isinstance(y[1], str) and isinstance(y[2], numbers.Number)
len(y) == 3 and isinstance(y[0], bool) and isinstance(
y[1], str) and isinstance(y[2], numbers.Number)
or len(y) == 2 and isinstance(y[0], str) and isinstance(y[1], numbers.Number)
for y in x),
expected_type=list
)
default_loras = [(y[0], y[1], y[2]) if len(y) == 3 else (True, y[0], y[1]) for y in default_loras]
default_loras = [(y[0], y[1], y[2]) if len(y) == 3 else (
True, y[0], y[1]) for y in default_loras]
default_max_lora_number = get_config_item_or_set_default(
key='default_max_lora_number',
default_value=len(default_loras) if isinstance(default_loras, list) and len(default_loras) > 0 else 5,
default_value=len(default_loras) if isinstance(
default_loras, list) and len(default_loras) > 0 else 5,
validator=lambda x: isinstance(x, int) and x >= 1,
expected_type=int
)
@ -380,7 +409,8 @@ default_styles = get_config_item_or_set_default(
"Fooocus Enhance",
"Fooocus Sharp"
],
validator=lambda x: isinstance(x, list) and all(y in modules.sdxl_styles.legal_style_names for y in x),
validator=lambda x: isinstance(x, list) and all(
y in modules.sdxl_styles.legal_style_names for y in x),
expected_type=list
)
default_prompt_negative = get_config_item_or_set_default(
@ -448,37 +478,43 @@ default_output_format = get_config_item_or_set_default(
default_image_number = get_config_item_or_set_default(
key='default_image_number',
default_value=2,
validator=lambda x: isinstance(x, int) and 1 <= x <= default_max_image_number,
validator=lambda x: isinstance(
x, int) and 1 <= x <= default_max_image_number,
expected_type=int
)
checkpoint_downloads = get_config_item_or_set_default(
key='checkpoint_downloads',
default_value={},
validator=lambda x: isinstance(x, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
validator=lambda x: isinstance(x, dict) and all(
isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
expected_type=dict
)
lora_downloads = get_config_item_or_set_default(
key='lora_downloads',
default_value={},
validator=lambda x: isinstance(x, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
validator=lambda x: isinstance(x, dict) and all(
isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
expected_type=dict
)
embeddings_downloads = get_config_item_or_set_default(
key='embeddings_downloads',
default_value={},
validator=lambda x: isinstance(x, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
validator=lambda x: isinstance(x, dict) and all(
isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
expected_type=dict
)
vae_downloads = get_config_item_or_set_default(
key='vae_downloads',
default_value={},
validator=lambda x: isinstance(x, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
validator=lambda x: isinstance(x, dict) and all(
isinstance(k, str) and isinstance(v, str) for k, v in x.items()),
expected_type=dict
)
available_aspect_ratios = get_config_item_or_set_default(
key='available_aspect_ratios',
default_value=modules.flags.sdxl_aspect_ratios,
validator=lambda x: isinstance(x, list) and all('*' in v for v in x) and len(x) > 1,
validator=lambda x: isinstance(x, list) and all(
'*' in v for v in x) and len(x) > 1,
expected_type=list
)
default_aspect_ratio = get_config_item_or_set_default(
@ -521,7 +557,8 @@ for image_count in range(default_controlnet_image_count):
default_ip_images[image_count] = get_config_item_or_set_default(
key=f'default_ip_image_{image_count}',
default_value='None',
validator=lambda x: x == 'None' or isinstance(x, str) and os.path.exists(x),
validator=lambda x: x == 'None' or isinstance(
x, str) and os.path.exists(x),
expected_type=str
)
@ -571,7 +608,8 @@ default_cfg_tsnr = get_config_item_or_set_default(
default_clip_skip = get_config_item_or_set_default(
key='default_clip_skip',
default_value=2,
validator=lambda x: isinstance(x, int) and 1 <= x <= modules.flags.clip_skip_max,
validator=lambda x: isinstance(
x, int) and 1 <= x <= modules.flags.clip_skip_max,
expected_type=int
)
default_overwrite_step = get_config_item_or_set_default(
@ -596,7 +634,8 @@ example_inpaint_prompts = get_config_item_or_set_default(
default_value=[
'highly detailed face', 'detailed girl face', 'detailed man face', 'detailed hand', 'beautiful eyes'
],
validator=lambda x: isinstance(x, list) and all(isinstance(v, str) for v in x),
validator=lambda x: isinstance(x, list) and all(
isinstance(v, str) for v in x),
expected_type=list
)
example_enhance_detection_prompts = get_config_item_or_set_default(
@ -604,9 +643,38 @@ example_enhance_detection_prompts = get_config_item_or_set_default(
default_value=[
'face', 'eye', 'mouth', 'hair', 'hand', 'body'
],
validator=lambda x: isinstance(x, list) and all(isinstance(v, str) for v in x),
validator=lambda x: isinstance(x, list) and all(
isinstance(v, str) for v in x),
expected_type=list
)
default_enhance_prompts = {
'face': {
'positive': "Enhance the face to ensure clear and detailed features. The face should have a well-defined structure with smooth skin, natural contours, and a balanced complexion. Make sure the expression is natural and engaging.",
'negative': "Avoid any blurriness or distortions in the face. Do not include uneven skin tones, unnatural facial expressions, or any missing facial features. Ensure there are no artifacts or unnatural smoothing that might distort the face's natural appearance."
},
'eye': {
'positive': "Enhance the eyes to be clear, sharp, and vividly detailed. The eyes should have natural reflections and a realistic appearance. Ensure the irises and pupils are distinct, and there are no shadows or blurs affecting the eyes.",
'negative': "Exclude any blurring, distortions, or unnatural reflections in the eyes. Avoid asymmetrical or misaligned eyes, and ensure there are no unnatural colors or artifacts that could detract from a realistic appearance."
},
'mouth': {
'positive': "Enhance the mouth to appear natural and symmetrical. The lips should be smooth and well-defined, with no abnormalities. Ensure the mouth reflects a realistic expression and that teeth are visible only if naturally exposed.",
'negative': "Avoid any distortions, asymmetry, or unnatural shapes in the mouth. Do not include missing or extra teeth, and ensure there are no anomalies or artifacts affecting the mouth's appearance."
},
'hair': {
'positive': "Enhance the hair to look full, natural, and well-styled. The texture should be realistic, with clear individual strands or locks and natural shine. Ensure the color and style match the intended look without any unnatural effects.",
'negative': "Exclude any unnatural textures, blurs, or artifacts in the hair. Avoid colors that look artificial or inconsistent, and ensure there are no missing or irregular sections of hair that could disrupt the natural appearance."
},
'hand': {
'positive': "Enhance the hands to ensure all fingers are clearly visible and well-defined. The hands should have realistic textures and proportions, with no missing or distorted fingers. The overall appearance should be natural and proportional.",
'negative': "Avoid any distortions or missing fingers in the hands. Do not include unnatural shapes or proportions, and ensure there are no anomalies or artifacts that affect the realistic appearance of the hands."
},
'body': {
'positive': "Enhance the body to ensure a complete and natural appearance with all limbs properly defined. The body should reflect realistic proportions and posture, with no missing or distorted body parts. Ensure the overall shape and anatomy are natural and well-balanced.",
'negative': "Exclude any missing limbs, distortions, or unrealistic body shapes. Avoid anomalies in body posture or proportions, and ensure there are no artifacts or inconsistencies that could affect the natural appearance of the body."
}
}
default_enhance_tabs = get_config_item_or_set_default(
key='default_enhance_tabs',
default_value=3,
@ -658,7 +726,8 @@ default_save_metadata_to_images = get_config_item_or_set_default(
default_metadata_scheme = get_config_item_or_set_default(
key='default_metadata_scheme',
default_value=MetadataScheme.FOOOCUS.value,
validator=lambda x: x in [y[1] for y in modules.flags.metadata_scheme if y[1] == x],
validator=lambda x: x in [y[1]
for y in modules.flags.metadata_scheme if y[1] == x],
expected_type=str
)
metadata_created_by = get_config_item_or_set_default(
@ -669,7 +738,8 @@ metadata_created_by = get_config_item_or_set_default(
)
example_inpaint_prompts = [[x] for x in example_inpaint_prompts]
example_enhance_detection_prompts = [[x] for x in example_enhance_detection_prompts]
example_enhance_detection_prompts = [[x]
for x in example_enhance_detection_prompts]
default_invert_mask_checkbox = get_config_item_or_set_default(
key='default_invert_mask_checkbox',
@ -719,7 +789,9 @@ default_describe_content_type = get_config_item_or_set_default(
expected_type=list
)
config_dict["default_loras"] = default_loras = default_loras[:default_max_lora_number] + [[True, 'None', 1.0] for _ in range(default_max_lora_number - len(default_loras))]
config_dict["default_loras"] = default_loras = default_loras[:default_max_lora_number] + \
[[True, 'None', 1.0]
for _ in range(default_max_lora_number - len(default_loras))]
# mapping config to meta parameter
possible_preset_keys = {
@ -759,7 +831,8 @@ REWRITE_PRESET = False
if REWRITE_PRESET and isinstance(args_manager.args.preset, str):
save_path = 'presets/' + args_manager.args.preset + '.json'
with open(save_path, "w", encoding="utf-8") as json_file:
json.dump({k: config_dict[k] for k in possible_preset_keys}, json_file, indent=4)
json.dump({k: config_dict[k]
for k in possible_preset_keys}, json_file, indent=4)
print(f'Preset saved to {save_path}. Exiting ...')
exit(0)
@ -772,13 +845,15 @@ def add_ratio(x):
default_aspect_ratio = add_ratio(default_aspect_ratio)
available_aspect_ratios_labels = [add_ratio(x) for x in available_aspect_ratios]
available_aspect_ratios_labels = [
add_ratio(x) for x in available_aspect_ratios]
# Only write config in the first launch.
if not os.path.exists(config_path):
with open(config_path, "w", encoding="utf-8") as json_file:
json.dump({k: config_dict[k] for k in always_save_keys}, json_file, indent=4)
json.dump({k: config_dict[k]
for k in always_save_keys}, json_file, indent=4)
# Always write tutorials.
@ -799,7 +874,8 @@ wildcard_filenames = []
def get_model_filenames(folder_paths, extensions=None, name_filter=None):
if extensions is None:
extensions = ['.pth', '.ckpt', '.bin', '.safetensors', '.fooocus.patch']
extensions = ['.pth', '.ckpt', '.bin',
'.safetensors', '.fooocus.patch']
files = []
if not isinstance(folder_paths, list):
@ -913,14 +989,16 @@ def downloading_ip_adapters(v):
model_dir=path_clip_vision,
file_name='clip_vision_vit_h.safetensors'
)
results += [os.path.join(path_clip_vision, 'clip_vision_vit_h.safetensors')]
results += [os.path.join(path_clip_vision,
'clip_vision_vit_h.safetensors')]
load_file_from_url(
url='https://huggingface.co/lllyasviel/misc/resolve/main/fooocus_ip_negative.safetensors',
model_dir=path_controlnet,
file_name='fooocus_ip_negative.safetensors'
)
results += [os.path.join(path_controlnet, 'fooocus_ip_negative.safetensors')]
results += [os.path.join(path_controlnet,
'fooocus_ip_negative.safetensors')]
if v == 'ip':
load_file_from_url(
@ -928,7 +1006,8 @@ def downloading_ip_adapters(v):
model_dir=path_controlnet,
file_name='ip-adapter-plus_sdxl_vit-h.bin'
)
results += [os.path.join(path_controlnet, 'ip-adapter-plus_sdxl_vit-h.bin')]
results += [os.path.join(path_controlnet,
'ip-adapter-plus_sdxl_vit-h.bin')]
if v == 'face':
load_file_from_url(
@ -936,7 +1015,8 @@ def downloading_ip_adapters(v):
model_dir=path_controlnet,
file_name='ip-adapter-plus-face_sdxl_vit-h.bin'
)
results += [os.path.join(path_controlnet, 'ip-adapter-plus-face_sdxl_vit-h.bin')]
results += [os.path.join(path_controlnet,
'ip-adapter-plus-face_sdxl_vit-h.bin')]
return results
@ -949,6 +1029,7 @@ def downloading_upscale_model():
)
return os.path.join(path_upscale_models, 'fooocus_upscaler_s409985e5.bin')
def downloading_safety_checker_model():
load_file_from_url(
url='https://huggingface.co/mashb1t/misc/resolve/main/stable-diffusion-safety-checker.bin',

View File

@ -8,17 +8,21 @@ upscale_15 = 'Upscale (1.5x)'
upscale_2 = 'Upscale (2x)'
upscale_fast = 'Upscale (Fast 2x)'
uov_list = [disabled, subtle_variation, strong_variation, upscale_15, upscale_2, upscale_fast]
uov_list = [disabled, subtle_variation, strong_variation,
upscale_15, upscale_2, upscale_fast]
enhancement_uov_before = "Before First Enhancement"
enhancement_uov_after = "After Last Enhancement"
enhancement_uov_processing_order = [enhancement_uov_before, enhancement_uov_after]
enhancement_uov_processing_order = [
enhancement_uov_before, enhancement_uov_after]
enhancement_uov_prompt_type_original = 'Original Prompts'
enhancement_uov_prompt_type_last_filled = 'Last Filled Enhancement Prompts'
enhancement_uov_prompt_types = [enhancement_uov_prompt_type_original, enhancement_uov_prompt_type_last_filled]
enhancement_uov_prompt_types = [
enhancement_uov_prompt_type_original, enhancement_uov_prompt_type_last_filled]
CIVITAI_NO_KARRAS = ["euler", "euler_ancestral", "heun", "dpm_fast", "dpm_adaptive", "ddim", "uni_pc"]
CIVITAI_NO_KARRAS = ["euler", "euler_ancestral", "heun",
"dpm_fast", "dpm_adaptive", "ddim", "uni_pc"]
# fooocus: a1111 (Civitai)
KSAMPLER = {
@ -55,7 +59,8 @@ SAMPLERS = KSAMPLER | SAMPLER_EXTRA
KSAMPLER_NAMES = list(KSAMPLER.keys())
SCHEDULER_NAMES = ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform", "lcm", "turbo", "align_your_steps", "tcd", "edm_playground_v2.5"]
SCHEDULER_NAMES = ["normal", "karras", "exponential", "sgm_uniform", "simple",
"ddim_uniform", "lcm", "turbo", "align_your_steps", "tcd", "edm_playground_v2.5"]
SAMPLER_NAMES = KSAMPLER_NAMES + list(SAMPLER_EXTRA.keys())
sampler_list = SAMPLER_NAMES
@ -68,7 +73,8 @@ default_vae = 'Default (model)'
refiner_swap_method = 'joint'
default_input_image_tab = 'uov_tab'
input_image_tab_ids = ['uov_tab', 'ip_tab', 'inpaint_tab', 'describe_tab', 'enhance_tab', 'metadata_tab']
input_image_tab_ids = ['uov_tab', 'ip_tab', 'inpaint_tab',
'describe_tab', 'enhance_tab', 'metadata_tab']
cn_ip = "ImagePrompt"
cn_ip_face = "FaceSwap"
@ -84,7 +90,8 @@ default_parameters = {
output_formats = ['png', 'jpeg', 'webp']
inpaint_mask_models = ['u2net', 'u2netp', 'u2net_human_seg', 'u2net_cloth_seg', 'silueta', 'isnet-general-use', 'isnet-anime', 'sam']
inpaint_mask_models = ['u2net', 'u2netp', 'u2net_human_seg',
'u2net_cloth_seg', 'silueta', 'isnet-general-use', 'isnet-anime', 'sam']
inpaint_mask_cloth_category = ['full', 'upper', 'lower']
inpaint_mask_sam_model = ['vit_b', 'vit_l', 'vit_h']
@ -92,14 +99,15 @@ inpaint_engine_versions = ['None', 'v1', 'v2.5', 'v2.6']
inpaint_option_default = 'Inpaint or Outpaint (default)'
inpaint_option_detail = 'Improve Detail (face, hand, eyes, etc.)'
inpaint_option_modify = 'Modify Content (add objects, change background, etc.)'
inpaint_options = [inpaint_option_default, inpaint_option_detail, inpaint_option_modify]
inpaint_options = [inpaint_option_default,
inpaint_option_detail, inpaint_option_modify]
describe_type_photo = 'Photograph'
describe_type_anime = 'Art/Anime'
describe_types = [describe_type_photo, describe_type_anime]
sdxl_aspect_ratios = [
'704*1408', '704*1344', '768*1344', '768*1280', '832*1216', '832*1152',
'512*512', '704*704', '704*1408', '704*1344', '768*1344', '768*1280', '832*1216', '832*1152',
'896*1152', '896*1088', '960*1088', '960*1024', '1024*1024', '1024*960',
'1088*960', '1088*896', '1152*896', '1152*832', '1216*832', '1280*768',
'1344*768', '1344*704', '1408*704', '1472*704', '1536*640', '1600*640',

View File

@ -0,0 +1,23 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url('material.woff2') format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}

Binary file not shown.

398
web/assets/css/styles.css Normal file
View File

@ -0,0 +1,398 @@
#chart-button {
position: sticky;
transition: background-color 0.3s ease, width 0.8s ease, height 0.8s ease, transform 0.5s ease;
background-color: #00000096 !important;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2) !important;
color: white !important;
border-radius: 10px !important;
z-index: 9998;
-webkit-app-region: drag; /* Make this container draggable */
width: 100% !important;
height: 100% !important;
transform: scale(0);
-webkit-user-select: none; /* For Chrome, Safari, and Opera */
-moz-user-select: none; /* For Firefox */
-ms-user-select: none; /* For Internet Explorer and Edge */
user-select: none; /* Standard syntax */
}
#chart-button-container {
top: 0px;
left: 0px;
height: 0% !important;
width: 0% !important;
transform: scale(0);
position: fixed;
z-index: 9991;
will-change: transform;
text-align: center;
transition: width 0.8s ease, height 0.8s ease, transform 0.3s ease;
}
#chart-button-container.active {
transform: scale(1);
}
#mydivheader {
padding: 10px;
cursor: move;
z-index: 10;
background-color: #2196f3;
color: #fff;
}
body {
transition: all 0.3s ease;
}
#chart-button.small {
transform: scale(0.83);
}
#chart-button.medium {
transform: scale(0.93);
}
#chart-button.large {
transform: scale(0.96);
}
#chart-button.bottom-right {
margin: 0px;
}
#chart-button.bottom-left {
margin: 0px;
}
#chart-button.bottom-center {
margin: 0px;
}
#chart-button.top-right {
margin: 0px;
}
#chart-button.top-left {
margin: 0px;
}
#chart-button.top-center {
margin: 0px;
}
#chart-button.left-center {
margin: 0px;
}
#chart-button.right-center {
margin: 0px;
}
#chart-button.center {
margin: 0px;
}
#chart-button.bottom-right.active {
margin: 0px;
}
#chart-button.bottom-left.active {
margin: 0px;
}
#chart-button.bottom-center.active {
margin: 0px;
}
#chart-button.top-right.active {
margin: 0px;
}
#chart-button.top-left.active {
margin: 0px;
}
#chart-button.top-center.active {
margin: 0px;
}
#chart-button.left-center.active {
margin: 0px;
}
#chart-button.right-center.active {
margin: 0px;
}
#chart-button.center.active {
margin: 0px;
}
.chart-row {
padding: 5px 5px 0px 5px !important;
text-align: right !important;
display: flex !important;
justify-content: space-evenly !important;
z-index: 9999 !important;
position: relative !important;
align-items: center;
margin-bottom: -10px;
}
.chart-row.no-drag {
-webkit-app-region: no-drag; /* Make these elements non-draggable */
}
.chart-col {
flex: auto !important;
}
.left-col a {
width: 15px !important;
cursor: pointer !important;
border-radius: 4px !important;
display: inline-block !important;
text-align: center !important;
color: #fff !important;
text-decoration: none !important;
}
.left-col a:hover {
background-color: #fff !important;
color: #000 !important;
}
#chart-container.bar.small {
padding: 5px !important;
}
#chart-container.bar.medium {
padding: 5px !important;
}
#chart-container.bar.large {
padding: 5px !important;
}
#chart-container.line.small {
padding: 5px !important;
}
#chart-container.line.medium {
padding: 5px !important;
}
#chart-container.line.large {
padding: 5px !important;
}
i {
color: #fff !important; /* Adjust color */
cursor: pointer !important; /* Change cursor to pointer on hover */
-webkit-app-region: no-drag; /* Make these elements non-draggable */
}
a,
button {
-webkit-app-region: no-drag; /* Make these elements non-draggable */
}
.toggle-resources-button:hover {
color: rgb(101, 101, 101) !important; /* Change color on hover */
}
.drag {
-webkit-app-region: drag; /* Make this container draggable */
}
.no-drag {
-webkit-app-region: no-drag; /* Make these elements non-draggable */
}
#settingsMenu {
display: grid !important; /* Show the menu */
position: absolute !important;
transform: scale(0) translateX(-100%) translateY(-200%) !important; /* Center alignment */
background: #000000 !important;
border: 0px solid #ddd !important;
border-radius: 6px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
opacity: 0 !important;
transition: opacity 0.5s ease, transform 0.3s ease !important;
text-align: center;
-webkit-app-region: no-drag; /* Make these elements non-draggable */
}
#settingsMenu.show {
min-width: 110px;
opacity: 1 !important;
z-index: 1000 !important;
transform: scale(0.8) translateY(-30px) translateX(-10px) !important; /* Animate to visible state */
}
.position-menu {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
grid-template-rows: repeat(3, 1fr) !important;
grid-gap: 0px !important;
}
.position-btn > i {
color: #fff !important;
}
.position-btn {
color: #fff !important;
padding: 6px !important;
margin: 0px !important;
font-size: 12px !important;
cursor: pointer !important;
border: 0px solid #ccc !important;
background-color: transparent !important;
border-radius: 4px !important;
text-align: center !important;
}
.position-btn:hover {
background-color: #fff !important;
}
.position-btn:hover > i {
color: #000 !important;
}
.material-icons {
font-size: 14px !important;
}
.left-col {
text-align: left !important;
margin-right: 10px;
}
.left-col.text {
margin-bottom: 5px !important;
}
.settings-row {
display: inline-block;
text-align: center !important;
margin: 5px !important;
}
.settings-col {
text-align: center;
font-size: 10px;
}
.settings-hr {
width: 0px;
margin-top: 0px !important;
margin-bottom: 4px !important;
transition: width 1s ease;
}
.settings-hr.show {
width: 100%;
}
#custom-legend {
display: flex !important;
text-align: center;
}
.custom-legend-item {
margin-bottom: 5px !important;
font-size: 14px !important;
flex: auto !important;
align-items: center;
}
.custom-legend-color {
display: inline-block !important;
width: 15px !important;
height: 15px !important;
margin-right: 10px !important;
vertical-align: middle !important;
}
.custom-legend-text {
display: inline !important;
vertical-align: middle !important;
}
#show_resource_monitor {
cursor: pointer !important;
z-index: 9991;
margin: 5px;
display: flex;
font-size: 12px;
align-items: center;
}
.resource-monitor-icon {
margin: 5px;
height: 14px;
}
/* Example CSS transition for smooth resizing */
.window {
transition: width 0.3s ease, height 0.3s ease;
}
#show_resource_monitor_link {
cursor: pointer !important;
position: fixed;
top: 20px;
left: 10px;
z-index: 9991;
margin: 5px;
display: flex;
font-size: 12px;
align-items: center;
}
.resource-monitor-icon {
margin: 5px;
height: 14px;
}
#item-table {
width: -webkit-fill-available;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 5px;
border-collapse: separate; /* Ensures that border-radius works */
border-spacing: 0; /* Removes spacing between cells */
border: 0px solid #ddd; /* Adds border to the table */
border-radius: 8px; /* Adjust the value for desired corner radius */
overflow: hidden; /* Ensures rounded corners are visible */
}
#item-table th,
#item-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
line-height: 10px;
}
#item-table thead {
background-color: #333;
color: #fff;
}
#item-table tbody tr:nth-child(even) {
background-color: rgba(255, 255, 255, 0.1);
}
#item-table tbody tr:nth-child(odd) {
background-color: transparent;
}
#item-table tbody td {
color: #fff;
}
#progress-bar-container {
width: -webkit-fill-available;
height: 20px; /* Height of the progress bar */
background-color: #e0e0e0; /* Light gray background */
border-radius: 10px; /* Rounded corners for the progress bar container */
margin: 0px 5px; /* Space between the progress bar and the table */
display: none;
}
#progress-bar {
height: 100%;
width: 0%; /* Start with 0% width */
background-color: #76c7c0; /* Fill color of the progress bar */
border-radius: 10px; /* Rounded corners for the progress bar */
transition: width 0.5s ease; /* Smooth transition effect */
}

BIN
web/assets/img/clearfix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,563 @@
// VARS AND CONST
const hostname = 'localhost' // Gets the host without port
const baseUrl = `http://${hostname}:5000` // Append the port 5000
const apiUrl = `${baseUrl}/gpu_usage/`
const colorPalette = [
'rgb(240, 193, 90, 0.2)',
'rgb(240, 142, 219, 0.2)',
'rgb(24, 90, 219, 0.2)',
'rgb(127, 161, 195, 0.2)',
'rgb(128, 239, 145, 0.2)',
'rgb(245, 245, 245, 0.2)',
'rgb(240, 142, 219, 0.2)',
'rgb(159, 238, 209, 0.2)',
]
const borderColors = [
'rgb(240, 193, 90)',
'rgb(240, 142, 219)',
'rgb(24, 90, 219)',
'rgb(127, 161, 195)',
'rgb(128, 239, 145)',
'rgb(245, 245, 245)',
'rgb(240, 142, 219)',
'rgb(159, 238, 209)',
]
let currentChart = null // Track the current chart instance
const MAX_DATA_POINTS = 50 // Number of data points to keep
// Custom plugin to draw fixed labels in the middle of the chart area
const fixedLabelPlugin = {
id: 'fixedLabelPlugin',
afterDatasetsDraw(chart) {
const { ctx, scales, data } = chart
ctx.save()
const centerX = scales.x.left + scales.x.width / 2
const labelPositions = []
data.datasets[0].data.forEach((value, index) => {
const yPos = chart.getDatasetMeta(0).data[index].y
// Store yPos for positioning labels
labelPositions.push({
x: centerX,
y: yPos,
value: `${+value.toFixed(2)}` + `${index == 5 ? '\u00B0' : '%'}`,
})
})
const size = localStorage.getItem('chart-size') ?? 'small'
let fontSize = 10 // Default font size
switch (size) {
case 'small':
fontSize = '7px'
break
case 'medium':
fontSize = '16px'
break
default:
fontSize = '18px'
break
}
ctx.font = fontSize
ctx.fillStyle = '#FFFFFF'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
labelPositions.forEach((label) => {
ctx.fillText(label.value, label.x, label.y)
})
ctx.restore()
},
}
// Initialize the bar chart
window.initializeBarChart = function () {
localStorage.setItem('active-chart', 'bar')
var table = document.getElementById('item-table')
table.style.display = 'none'
const chartContainer = document.getElementById('chart-container')
const existingCanvas = document.getElementById('usage-chart')
const chartWrapper = document.getElementById('chart-wrapper')
if (existingCanvas) {
chartContainer.removeChild(existingCanvas)
}
const size = localStorage.getItem('chart-size') ?? 'small'
let fontSize = 10 // Default font size
switch (size) {
case 'small':
fontSize = '7px'
break
case 'medium':
fontSize = '16px'
break
default:
fontSize = '18px'
break
}
// Create a new canvas element
const newCanvas = document.createElement('canvas')
newCanvas.id = 'usage-chart'
newCanvas.classList.add('bar') // Add the class directly to the canvas element
chartContainer.appendChild(newCanvas)
const ctx = newCanvas.getContext('2d')
$(chartWrapper).hide()
currentChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['CPU', 'RAM', 'GPU', 'VRAM', 'HDD', 'TEMP'],
datasets: [
{
label: 'Usage',
data: [0, 0, 0, 0, 0],
barPercentage: 0.8, // Adjust space occupied by bars
categoryPercentage: 1, // Adjust space between bars
backgroundColor: function (context) {
const value = context.dataset.data[context.dataIndex]
return value > 90 ? '#D9534F' : colorPalette[context.dataIndex]
},
borderColor: function (context) {
const value = context.dataset.data[context.dataIndex]
return value > 90 ? '#D9534F' : borderColors[context.dataIndex]
},
borderWidth: 1.5,
},
],
},
options: {
indexAxis: 'y', // Horizontal bars
scales: {
x: {
grid: {
display: false, // Hide all grid lines
},
border: {
display: false, // Hide all grid lines
},
beginAtZero: true,
max: 100,
ticks: {
color: '#ffffff',
font: {
size: fontSize,
weight: 600,
},
align: 'center',
callback: function (value, index, ticks) {
return value + '%'
},
},
},
y: {
grid: {
display: false,
},
border: {
color: '#ffffff30',
width: 1, // Width of the axis border
},
ticks: {
color: '#FFFFFF',
crossAlign: 'far',
font: {
weight: 600,
size: fontSize,
},
// Specify the maximum number of ticks to show
maxTicksLimit: 10,
// Control the step size between ticks
stepSize: 1,
// Optional: Set font size and other style properties
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
responsive: true,
maintainAspectRatio: false,
},
plugins: [fixedLabelPlugin], // Register the custom plugins
})
currentChart.options.animation = true
const legendContainer = document.getElementById('custom-legend')
legendContainer.innerHTML = ''
document.getElementById('settingsMenu').classList.remove('show') // Hide the menu
document.querySelectorAll('canvas').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
window.addEventListener('resize', () => {
currentChart.resize()
})
$(chartWrapper).fadeIn(300)
if (size == 'large') {
$(table).fadeIn(800)
}
}
// Initialize the line chart
window.initializeLineChart = function () {
localStorage.setItem('active-chart', 'line')
var table = document.getElementById('item-table')
table.style.display = 'none'
const size = localStorage.getItem('chart-size') ?? 'medium'
const existingCanvas = document.getElementById('usage-chart')
const chartContainer = document.getElementById('chart-container')
const chartWrapper = document.getElementById('chart-wrapper')
if (existingCanvas) {
chartContainer.removeChild(existingCanvas)
}
// Create a new canvas element
const newCanvas = document.createElement('canvas')
newCanvas.id = 'usage-chart'
newCanvas.classList.add('line') // Add the class directly to the canvas element
chartContainer.appendChild(newCanvas)
$(chartWrapper).hide()
const ctx = newCanvas.getContext('2d')
currentChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'CPU',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
{
label: 'RAM',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
{
label: 'GPU',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
{
label: 'VRAM',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
{
label: 'HDD',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
{
label: 'TEMP',
data: [],
borderColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return borderColors[datasetIndex % borderColors.length]
},
borderWidth: 1.5,
backgroundColor: function (context) {
const dataset = context.dataset
const datasetIndex = context.datasetIndex
const shouldUseRed = dataset.data.some((value) => value > 90)
if (shouldUseRed) {
return '#D9534F' // Return red color if any value exceeds 90
}
return colorPalette[datasetIndex % borderColors.length]
},
fill: false,
tension: 0.1,
},
],
},
options: {
animation: {
enabled: false,
tension: {
duration: 1000,
easing: 'linear',
from: 1,
to: 0,
loop: true,
},
},
elements: {
point: {
radius: 0,
},
},
scales: {
x: {
ticks: {
display: false,
},
},
y: {
beginAtZero: true,
max: 100,
ticks: {
color: '#FFFFFF',
crossAlign: 'far',
padding: 0,
font: {
weight: 600,
size: 7,
},
callback: function (value, index, ticks) {
return value + '%'
},
},
},
},
responsive: true,
plugins: {
legend: {
display: true,
labels: {
generateLabels: false,
},
},
title: {
display: false,
},
},
},
})
currentChart.options.animation = false
generateCustomLegend()
document.getElementById('settingsMenu').classList.remove('show') // Hide the menu
window.addEventListener('resize', () => {
currentChart.resize()
})
$(chartWrapper).fadeIn(300)
if (size == 'large') {
$(table).fadeIn(800)
}
}
function generateCustomLegend() {
const legendContainer = document.getElementById('custom-legend')
legendContainer.innerHTML = ''
currentChart.data.datasets.forEach((dataset, index) => {
const legendItem = document.createElement('div')
legendItem.className = 'custom-legend-item'
// Create text element
const legendText = document.createElement('span')
legendText.className = 'custom-legend-text'
legendText.textContent = dataset.label
const shouldUseRed = dataset.data.some((value) => value > 90)
legendText.style.color = shouldUseRed ? '#D9534F' : `${borderColors[index]}`
legendText.style.fontWeight = shouldUseRed ? '800' : `600`
const size = localStorage.getItem('chart-size') ?? 'small'
switch (size) {
case 'small':
legendText.style.fontSize = '7px'
break
case 'medium':
legendText.style.fontSize = '16px'
break
default:
legendText.style.fontSize = '18px'
break
}
legendItem.appendChild(legendText)
legendContainer.appendChild(legendItem)
})
}
setTimeout(() => {
const socket = io('http://localhost:5000')
// Function to update the chart with new data
function updateChart(data) {
const timestamp = new Date().toLocaleTimeString()
if (currentChart) {
if (currentChart.config.type === 'bar') {
// Update data for bar chart
currentChart.data.datasets[0].data = [
data.cpu,
data.ram,
data.gpu,
data.vram,
data.hdd,
data.temp,
]
} else if (currentChart.config.type === 'line') {
// Update data for line chart
currentChart.data.labels.push(timestamp)
currentChart.data.datasets[0].data.push(data.cpu)
currentChart.data.datasets[1].data.push(data.ram)
currentChart.data.datasets[2].data.push(data.gpu)
currentChart.data.datasets[3].data.push(data.vram)
currentChart.data.datasets[4].data.push(data.hdd)
currentChart.data.datasets[5].data.push(data.temp)
// Prune old data if the number of points exceeds the limit
const MAX_DATA_POINTS = 50 // Adjust as needed
if (currentChart.data.labels.length > MAX_DATA_POINTS) {
currentChart.data.labels.shift() // Remove the oldest label
currentChart.data.datasets.forEach((dataset) => dataset.data.shift()) // Remove the oldest data points
}
generateCustomLegend()
}
// Update the chart with new data
currentChart.update()
}
}
// Handle data updates from the WebSocket server
socket.on('data_update', function (data) {
updateChart(data)
})
// Optional: Handle WebSocket connection errors
socket.on('connect_error', function (error) {
console.error('Connection error:', error)
})
// Optional: Handle WebSocket disconnection
socket.on('disconnect', function () {
console.log('WebSocket disconnected')
})
}, 4000)

12536
web/assets/js/chart.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
var footer = document.querySelector('footer')
var link = document.createElement('a')
// Add multiple classes correctly using the spread operator
link.classList.add('built-with', 'svelte-1ax1toq')
link.id = 'show_resource_monitor'
link.text = 'Resource Monitor'
link.onclick = function () {
showPerfMonitor()
} // Use function reference instead of string
var linkImg = document.createElement('img')
linkImg.src = '/file=web/assets/img/monitor.svg'
linkImg.classList.add('svelte-1ax1toq')
link.appendChild(linkImg)
footer.appendChild(link)
var script = document.createElement('script')
script.src = '/file=web/assets/js/jquery-3.7.1.min.js'
document.body.appendChild(script)
var script = document.createElement('script')
script.src = '/file=web/assets/js/elegant-resource-monitor.js'
document.body.appendChild(script)
var script = document.createElement('script')
script.src = '/file=web/assets/js/socket.io.min.js'
document.body.appendChild(script)
var script = document.createElement('script')
script.src = '/file=web/assets/js/chart.js'
document.body.appendChild(script)
var fa = document.createElement('link')
fa.href = '/file=web/assets/css/material-icon.css'
fa.property = 'stylesheet'
fa.rel = 'stylesheet'
document.body.appendChild(fa)
var styles = document.createElement('link')
styles.href = '/file=web/assets/css/styles.css'
styles.property = 'stylesheet'
styles.rel = 'stylesheet'
document.body.appendChild(styles)
styles.onload = async function () {
if (localStorage.getItem('lastClass') && localStorage.getItem('lastInactiveClass')) {
var lastClass = JSON.parse(localStorage.getItem('lastClass'))
var lastInactiveClass = JSON.parse(localStorage.getItem('lastInactiveClass'))
addCSS(lastInactiveClass.key, lastInactiveClass.values[0])
addCSS(lastClass.key, lastClass.values[0])
}
function getCSSRule(ruleName) {
ruleName = ruleName.toLowerCase()
var result = null
var find = Array.prototype.find
Array.prototype.find.call(document.styleSheets, (styleSheet) => {
try {
if (styleSheet.cssRules) {
result = find.call(styleSheet.cssRules, (cssRule) => {
return cssRule instanceof CSSStyleRule && cssRule.selectorText.toLowerCase() == ruleName
})
}
} catch (e) {
// Handle cross-origin or other access errors
// console.info("Cannot access cssRules for stylesheet:", e);
}
return result != null
})
return result
}
function addCSS(selector, styles) {
var rule = getCSSRule(selector)
for (var property in styles) {
if (styles.hasOwnProperty(property)) {
rule.style.setProperty(property, styles[property], 'important')
}
}
}
async function loadHtmlContent() {
const response = await fetch('/file=web/templates/perf-monitor/perf-monitor.html')
var resourceMonitorContent = document.getElementById('perf-monitor-container')
resourceMonitorContent.innerHTML = await response.text()
const chartButton = resourceMonitorContent.querySelector('#chart-button')
const savedPosition = localStorage.getItem('perf-monitor-position') || 'bottom-right'
if (chartButton) {
// Set the savedPosition class on the #chart-button element
chartButton.classList.add(savedPosition)
}
var script = document.createElement('script')
script.src = '/file=web/assets/js/script.js'
document.body.appendChild(script)
var chart = document.createElement('script')
chart.src = '/file=web/assets/js/chart-settings.js'
document.body.appendChild(chart)
}
await loadHtmlContent()
}

View File

@ -0,0 +1,211 @@
import { api } from '/scripts/api.js'
import { getResolver } from './shared_utils.js'
export class ElegantResourceMonitorExecution {
constructor(id) {
this.promptApi = null
this.executedNodeIds = []
this.totalNodes = 0
this.currentlyExecuting = null
this.errorDetails = null
this.apiPrompt = getResolver()
this.id = id
}
setPrompt(prompt) {
this.promptApi = prompt.output
this.totalNodes = Object.keys(this.promptApi).length
this.apiPrompt.resolve(null)
}
getApiNode(nodeId) {
var _a
return ((_a = this.promptApi) === null || _a === void 0 ? void 0 : _a[String(nodeId)]) || null
}
getNodeLabel(nodeId) {
var _a, _b
const apiNode = this.getApiNode(nodeId)
let label =
((_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode._meta) === null ||
_a === void 0
? void 0
: _a.title) ||
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ||
undefined
if (!label) {
const graphNode =
(_b = this.maybeGetComfyGraph()) === null || _b === void 0
? void 0
: _b.getNodeById(Number(nodeId))
label =
(graphNode === null || graphNode === void 0 ? void 0 : graphNode.title) ||
(graphNode === null || graphNode === void 0 ? void 0 : graphNode.type) ||
undefined
}
return label
}
executing(nodeId, step, maxSteps) {
var _a
if (nodeId == null) {
this.currentlyExecuting = null
return
}
if (
((_a = this.currentlyExecuting) === null || _a === void 0 ? void 0 : _a.nodeId) !== nodeId
) {
if (this.currentlyExecuting != null) {
this.executedNodeIds.push(nodeId)
}
this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 }
this.apiPrompt.promise.then(() => {
var _a
if (this.currentlyExecuting == null) {
return
}
const apiNode = this.getApiNode(nodeId)
if (!this.currentlyExecuting.nodeLabel) {
this.currentlyExecuting.nodeLabel = this.getNodeLabel(nodeId)
}
if (
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ===
'UltimateSDUpscale'
) {
this.currentlyExecuting.pass--
this.currentlyExecuting.maxPasses = -1
} else if (
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ===
'IterativeImageUpscale'
) {
this.currentlyExecuting.maxPasses =
(_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode.inputs['steps']) !==
null && _a !== void 0
? _a
: -1
}
})
}
if (step != null) {
if (!this.currentlyExecuting.step || step < this.currentlyExecuting.step) {
this.currentlyExecuting.pass++
}
this.currentlyExecuting.step = step
this.currentlyExecuting.maxSteps = maxSteps
}
}
error(details) {
this.errorDetails = details
}
maybeGetComfyGraph() {
var _a
return (
((_a = window === null || window === void 0 ? void 0 : window.app) === null || _a === void 0
? void 0
: _a.graph) || null
)
}
}
class ElegantResourceMonitorService extends EventTarget {
constructor(api) {
super()
this.promptsMap = new Map()
this.currentExecution = null
this.lastQueueRemaining = 0
const that = this
const queuePrompt = api.queuePrompt
api.queuePrompt = async function (num, prompt) {
let response
try {
response = await queuePrompt.apply(api, [...arguments])
} catch (e) {
const promptExecution = that.getOrMakePrompt('error')
promptExecution.error({ exception_type: 'Unknown.' })
throw e
}
const promptExecution = that.getOrMakePrompt(response.prompt_id)
promptExecution.setPrompt(prompt)
if (!that.currentExecution) {
that.currentExecution = promptExecution
}
that.promptsMap.set(response.prompt_id, promptExecution)
return response
}
api.addEventListener('status', (e) => {
var _a
if (!((_a = e.detail) === null || _a === void 0 ? void 0 : _a.exec_info)) return
this.lastQueueRemaining = e.detail.exec_info.queue_remaining
this.dispatchProgressUpdate()
})
api.addEventListener('execution_start', (e) => {
if (!this.promptsMap.has(e.detail.prompt_id)) {
console.warn("'execution_start' fired before prompt was made.")
}
const prompt = this.getOrMakePrompt(e.detail.prompt_id)
this.currentExecution = prompt
this.dispatchProgressUpdate()
})
api.addEventListener('executing', (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt('unknown')
console.warn("'executing' fired before prompt was made.")
}
this.currentExecution.executing(e.detail)
this.dispatchProgressUpdate()
if (e.detail == null) {
this.currentExecution = null
}
})
api.addEventListener('progress', (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
console.warn("'progress' fired before prompt was made.")
}
this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max)
this.dispatchProgressUpdate()
})
api.addEventListener('execution_cached', (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
console.warn("'execution_cached' fired before prompt was made.")
}
for (const cached of e.detail.nodes) {
this.currentExecution.executing(cached)
}
this.dispatchProgressUpdate()
})
api.addEventListener('executed', (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
console.warn("'executed' fired before prompt was made.")
}
})
api.addEventListener('execution_error', (e) => {
var _a
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
console.warn("'execution_error' fired before prompt was made.")
}
;(_a = this.currentExecution) === null || _a === void 0 ? void 0 : _a.error(e.detail)
this.dispatchProgressUpdate()
})
}
async queuePrompt(prompt) {
return await api.queuePrompt(-1, prompt)
}
dispatchProgressUpdate() {
this.dispatchEvent(
new CustomEvent('elegant-resource-monitor-update', {
detail: {
queue: this.lastQueueRemaining,
prompt: this.currentExecution,
},
}),
)
}
getOrMakePrompt(id) {
let prompt = this.promptsMap.get(id)
if (!prompt) {
prompt = new ElegantResourceMonitorExecution(id)
this.promptsMap.set(id, prompt)
}
return prompt
}
}
export const MONITOR_SERVICE = new ElegantResourceMonitorService(api)

View File

@ -0,0 +1,288 @@
class ElegantResourceMonitor extends HTMLElement {
constructor() {
super()
this.shadow = null
this.currentPromptExecution = null
this.onProgressUpdateBound = this.onProgressUpdate.bind(this)
this.connected = false
}
render() {
this.innerHTML = `
<div id="chart-button-container" draggable="true">
<div id="chart-button">
<div class="chart-row">
<div class="left-col">
<i class="material-icons" id="popupTrigger">settings</i>
<div id="settingsMenu" class="settings-menu">
<div class="settings-row"><div class="settings-col">Settings</div></div>
<hr id="settings-hr" class="settings-hr" />
<div class="settings-row">
<div class="settings-row">
<div class="settings-col">Layout:</div>
<div class="settings-col">
<a href="#" onclick="barChart()">1</a> |
<a href="#" onclick="lineChart()">2</a>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Size:</div>
<div class="settings-col">
<a href="#" onclick="smallChart()">S</a> |
<a href="#" onclick="mediumChart()">M</a>
</div>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Position</div>
<div id="positionMenu" class="position-menu">
<button class="position-btn position-clickable" id="top-left"><i class="material-icons">north_west</i></button>
<button class="position-btn position-clickable" id="top-center"><i class="material-icons">north</i></button>
<button class="position-btn position-clickable" id="top-right"><i class="material-icons">north_east</i></button>
<button class="position-btn position-clickable" id="left-center"><i class="material-icons">west</i></button>
<button class="position-btn position-clickable" id="center" onclick="largeChart()"><i class="material-icons">radio_button_checked</i></button>
<button class="position-btn position-clickable" id="right-center"><i class="material-icons">east</i></button>
<button class="position-btn position-clickable" id="bottom-left"><i class="material-icons">south_west</i></button>
<button class="position-btn position-clickable" id="bottom-center"><i class="material-icons">south</i></button>
<button class="position-btn position-clickable" id="bottom-right"><i class="material-icons">south_east</i></button>
</div>
</div>
</div>
</div>
<div class="chart-col">
<i class="material-icons" id="close-button">close</i>
</div>
</div>
<div id="chart-wrapper">
<div id="progress-bar-container">
<div id="progress-bar"></div>
</div>
<div id="chart-container">
<canvas id="usage-chart" style="width: 100%; height: 100%"></canvas>
</div>
<div id="custom-legend"></div>
<div id="table-view">
<table id="item-table">
<tbody id="item-body">
<!-- Table rows will be dynamically added here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
`
}
addEventListeners() {
this.querySelector('#popupTrigger').addEventListener('click', () => {
const settingsMenu = this.querySelector('#settingsMenu')
settingsMenu.style.display = settingsMenu.style.display === 'block' ? 'none' : 'block'
})
this.querySelector('#close-button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))
})
}
getSizes() {
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
var sizes = {}
if (savedChart == 'bar') {
sizes = {
small: { height: '120', width: '150' },
medium: { height: '300', width: '410' },
large: { height: '450', width: '700' },
}
} else {
sizes = {
small: { height: '110', width: '160' },
medium: { height: '245', width: '425' },
large: { height: '380', width: '700' },
}
}
return sizes
}
getButtonSize() {
const size = localStorage.getItem('chart-size') ?? 'medium'
const sizes = this.getSizes()
const sizeStyles = sizes[size]
var buttonHeight = +sizeStyles.height + 25
const buttonWidth = +sizeStyles.width + 25
var table = this.querySelector('#item-table')
var totalRowCount = table.rows.length // 5
var tableHeight = totalRowCount * 30
if (size == 'large') {
buttonHeight = +buttonHeight + tableHeight
}
return { buttonHeight, buttonWidth }
}
addListItem(itemContent) {
const itemBody = this.querySelector('#item-body')
const row = document.createElement('tr')
const cell = document.createElement('td')
cell.innerText = itemContent
row.appendChild(cell)
itemBody.appendChild(row)
}
get currentNodeId() {
var _a, _b
const prompt = this.currentPromptExecution
const nodeId =
((_a = prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) === null ||
_a === void 0
? void 0
: _a.node_id) ||
((_b = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) === null ||
_b === void 0
? void 0
: _b.nodeId)
return nodeId || null
}
adjustButtonHeight(e) {
const chartButtonContainer = this.querySelector('#chart-button-container')
const shouldShowPerfMonitor = JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false
if (!shouldShowPerfMonitor || chartButtonContainer.clientHeight == 0) return
const { buttonHeight, buttonWidth } = this.getButtonSize()
const chartContainer = this.querySelector('#chart-container')
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
const size = localStorage.getItem('chart-size') ?? 'medium'
const height = this.querySelector('#chart-container').style.height.replace('px', '')
let bar = this.querySelector('#progress-bar-container')
var actulaButtonHeight = 0
var actualButtonWidth = 0
var viewportHeight = +buttonHeight
var viewportWidth = +buttonWidth
$(chartButtonContainer).each(function () {
this.style.setProperty('height', `${viewportHeight}px`, 'important')
this.style.setProperty('width', `${viewportWidth}px`, 'important')
})
var table = this.querySelector('#item-table')
var totalRowCount = table.rows.length // 5
var tableHeight = totalRowCount * 35
var workArea = actulaButtonHeight
if (size == 'large') {
workArea = viewportHeight - tableHeight
}
const prompt = e.detail.prompt
if (prompt && prompt.currentlyExecuting) {
bar.style.display = 'block'
workArea = workArea - 30
} else {
bar.style.display = 'none'
}
if (savedChart == 'bar') {
$(chartContainer).each(function () {
this.style.setProperty('height', `${workArea * 0.95}px`, 'important')
})
} else {
$(chartContainer).each(function () {
this.style.setProperty('height', `${workArea * 0.87}px`, 'important')
})
}
}
onProgressUpdate(e) {
// this.adjustButtonHeight(e)
var _a, _b, _c, _d
if (!this.connected) return
const prompt = e.detail.prompt
this.currentPromptExecution = prompt
// Default progress to 0 if no totalNodes
let progressPercentage = 0
if (prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) {
let progressText = `${
(_a = prompt.errorDetails) === null || _a === void 0 ? void 0 : _a.exception_type
} ${((_b = prompt.errorDetails) === null || _b === void 0 ? void 0 : _b.node_id) || ''} ${
((_c = prompt.errorDetails) === null || _c === void 0 ? void 0 : _c.node_type) || ''
}`
console.log(progressText)
// Set the progress bar to 0% or some error state if needed
this.querySelector('#progress-bar').style.width = '0%'
return
}
if (prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) {
const current = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting
let progressText = `(${e.detail.queue}) `
if (!prompt.totalNodes) {
progressText += `??%`
} else {
progressPercentage = (prompt.executedNodeIds.length / prompt.totalNodes) * 100
progressText += `${Math.round(progressPercentage)}%`
}
let nodeLabel = (_d = current.nodeLabel) === null || _d === void 0 ? void 0 : _d.trim()
const monitor = document.querySelector('elegant-resource-monitor')
// monitor.addListItem(nodeLabel)
let stepsLabel = ''
if (current.step != null && current.maxSteps) {
const percent = (current.step / current.maxSteps) * 100
if (current.pass > 1 || current.maxPasses != null) {
stepsLabel += `#${current.pass}`
if (current.maxPasses && current.maxPasses > 0) {
stepsLabel += `/${current.maxPasses}`
}
stepsLabel += ` - `
}
stepsLabel += `${Math.round(percent)}%`
}
if (nodeLabel || stepsLabel) {
progressText += ` - ${nodeLabel || '???'}${stepsLabel ? ` (${stepsLabel})` : ''}`
}
console.log(progressText)
} else {
if (e === null || e === void 0 ? void 0 : e.detail.queue) {
console.log(`(${e.detail.queue}) Running... in another tab`)
} else {
console.log('Idle')
}
}
// Update the progress bar width
this.querySelector('#progress-bar').style.width = `${Math.round(progressPercentage)}%`
}
connectedCallback() {
if (!this.connected) {
console.log('Adding event listener to MONITOR_SERVICE')
// MONITOR_SERVICE.addEventListener(
// 'elegant-resource-monitor-update',
// this.onProgressUpdateBound,
// )
this.render()
this.addEventListeners()
this.connected = true
}
}
disconnectedCallback() {
this.connected = false
// MONITOR_SERVICE.removeEventListener(
// 'elegant-resource-monitor-update',
// this.onProgressUpdateBound,
// )
}
}
// Register the custom element
customElements.define('elegant-resource-monitor', ElegantResourceMonitor)

2
web/assets/js/jquery-3.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

689
web/assets/js/script.js Normal file
View File

@ -0,0 +1,689 @@
dragElement(document.getElementById('chart-button-container'))
var wasDragged = false
function dragElement(elmnt) {
var isDragging = false
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown
function dragMouseDown(e) {
e = e || window.event
e.preventDefault()
// get the mouse cursor position at startup:
pos3 = e.clientX
pos4 = e.clientY
document.onmouseup = closeDragElement
// call a function whenever the cursor moves:
document.onmousemove = elementDrag
elmnt.style.cursor = 'grabbing'
}
function elementDrag(e) {
e = e || window.event
e.preventDefault()
// calculate the new cursor position:
pos1 = pos3 - e.clientX
pos2 = pos4 - e.clientY
pos3 = e.clientX
pos4 = e.clientY
// set the element's new position:
wasDragged = true
isDragging = true
elmnt.style.top = elmnt.offsetTop - pos2 + 'px'
elmnt.style.left = elmnt.offsetLeft - pos1 + 'px'
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null
document.onmousemove = null
elmnt.style.transition = 'transform .7s ease-in-out'
elmnt.style.cursor = 'grab'
setTimeout(() => {
if (!isDragging) {
elmnt.style.cursor = 'auto'
}
}, 1000)
isDragging = false
getNearestPosition()
}
}
function moveButtonToCenter(duration = 300) {
const { buttonHeight, buttonWidth } = getButtonSize()
// Get button dimensions and viewport dimensions
const widgetWidth = buttonWidth
const widgetHeight = buttonHeight
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// Calculate center of the viewport
const windowCenterX = viewportWidth / 2
const windowCenterY = viewportHeight / 2
// Calculate button center
const buttonCenterX = widgetWidth / 2
const buttonCenterY = widgetHeight / 2
// Calculate the translation offsets needed to center the button
const posx = windowCenterX - buttonCenterX
const posy = windowCenterY - buttonCenterY
goToPosition({ x: posx, y: posy })
}
// Call the function to move the button
// HELPER FUNCTIONS //
function getCSSRule(ruleName) {
ruleName = ruleName.toLowerCase()
var result = null
var find = Array.prototype.find
Array.prototype.find.call(document.styleSheets, (styleSheet) => {
try {
if (styleSheet.cssRules) {
result = find.call(styleSheet.cssRules, (cssRule) => {
return cssRule instanceof CSSStyleRule && cssRule.selectorText.toLowerCase() == ruleName
})
}
} catch (e) {
// Handle cross-origin or other access errors
// console.info("Cannot access cssRules for stylesheet:", e);
}
return result != null
})
return result
}
window.barChart = async function () {
checkForUpdates('active-chart', 'bar')
await updateChartSize()
}
window.lineChart = async function () {
checkForUpdates('active-chart', 'line')
await updateChartSize()
}
window.smallChart = async function () {
checkForUpdates('chart-size', 'small')
await updateChartSize()
}
window.mediumChart = async function () {
checkForUpdates('chart-size', 'medium')
await updateChartSize()
}
window.largeChart = async function () {
setTimeout(async () => {
checkForUpdates('perf-monitor-position', 'center')
checkForUpdates('chart-size', 'large')
await updateChartSize()
}, 50)
}
function moveToCenter() {
if (localStorage.getItem('perf-monitor-position') === 'center') {
moveButtonToCenter(150)
}
}
function checkForUpdates(key, value) {
var previous = localStorage.getItem(key)
var updated = previous != value
localStorage.setItem('hasUpdates', updated)
localStorage.setItem(key, value)
}
function isWindowOutsideWorkingArea() {
const { buttonHeight, buttonWidth } = getButtonSize()
// Get display bounds
const { displayBounds } = getDisplayAndWindowBounds()
const widget = document.getElementById('chart-button-container')
const rect = widget.getBoundingClientRect()
const currentTop = rect.top + window.scrollY
const currentLeft = rect.left + window.scrollX
const windowLeft = currentLeft
const windowTop = currentTop
const windowRight = windowLeft + buttonWidth
const windowBottom = windowTop + buttonHeight
const displayLeft = 0
const displayTop = 0
const displayRight = displayLeft + displayBounds.width
const displayBottom = displayTop + displayBounds.height
let isOutside =
windowLeft < displayLeft ||
windowTop < displayTop ||
windowRight > displayRight ||
windowBottom > displayBottom
if (isOutside) {
console.log('The window is outside the working area.')
} else {
console.log('The window is within the working area.')
}
return isOutside
}
function getSizes() {
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
var sizes = {}
if (savedChart == 'bar') {
sizes = {
small: { height: '120', width: '150' },
medium: { height: '300', width: '410' },
large: { height: '450', width: '700' },
}
} else {
sizes = {
small: { height: '110', width: '160' },
medium: { height: '245', width: '425' },
large: { height: '380', width: '700' },
}
}
return sizes
}
// SETTINGS MENU //
// POSITIONS BUTTONS
document.querySelectorAll('.position-clickable').forEach((button) => {
button.addEventListener('click', async function () {
const position = this.id
wasDragged = false
localStorage.setItem('perf-monitor-position', position)
//the position we should be going to
const pos = getCoordinates(false)
if (pos) {
goToPosition(pos)
} else {
console.error('Invalid position:', pos)
}
// Optionally hide the settings menu and adjust UI
const settingsMenu = document.getElementById('settingsMenu')
settingsMenu.classList.remove('show') // Hide the menu if visible
document.querySelectorAll('.chart-row').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
})
})
// Show or hide the settings menu when the settings icon is clicked
document.getElementById('popupTrigger').addEventListener('click', function (event) {
const settingsMenu = document.getElementById('settingsMenu')
settingsMenu.classList.toggle('show') // Toggle the 'show' class for animation
document.querySelectorAll('.chart-row').forEach((row) => {
row.classList.add('no-drag')
row.classList.remove('drag')
})
document.querySelectorAll('canvas').forEach((row) => {
row.classList.add('no-drag')
row.classList.remove('drag')
})
setTimeout(() => {
const settingsMenuHr = document.getElementById('settings-hr')
settingsMenuHr.classList.add('show') // Toggle the 'show' class for animation
}, 300)
event.stopPropagation()
})
// Hide the settings menu when clicking outside
window.addEventListener('click', function (e) {
if (e.target.className.includes('settings')) {
return
}
const settingsMenu = document.getElementById('settingsMenu')
const trigger = document.getElementById('popupTrigger')
if (!settingsMenu.contains(e.target) && e.target !== trigger) {
settingsMenu.classList.remove('show') // Hide the menu if clicking outside
}
document.querySelectorAll('canvas').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
document.querySelectorAll('.chart-row').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
})
// Calculate if the menu will overflow the bottom of the viewport
document.getElementById('popupTrigger').addEventListener('click', function () {
const menu = document.getElementById('settingsMenu')
const menuRect = menu.getBoundingClientRect()
const buttonRect = this.getBoundingClientRect()
const viewportHeight = window.innerHeight
if (menu.offsetTop < 0) {
menu.style.position = 'absolute'
menu.style.top = `29px`
}
let topPosition = buttonRect.bottom
if (topPosition + menuRect.height > viewportHeight) {
// Calculate how much the menu overflows the viewport
const overflowAmount = topPosition + menuRect.height - viewportHeight
// Apply the calculated position
menu.style.position = 'absolute' // Ensure the menu is positioned absolutely
menu.style.top = `-${overflowAmount}px`
}
})
function goToPosition(pos) {
const widget = document.getElementById('chart-button-container')
// Set transition for smooth animation
// widget.style.transition = 'transform .7s ease-in-out'
widget.style.transition = `top .4s ease, left .4s ease`
const currentTop = +widget.style.top.replace('px', '')
const currentLeft = +widget.style.left.replace('px', '')
// Target position
const offsetX = pos.x - currentLeft
const offsetY = pos.y - currentTop
// Set transition duration and easing
widget.style.transition = `transform .7s ease-in-out`
// Animate to the center
widget.style.transform = `translate(${offsetX}px, ${offsetY}px)`
}
// MAIN METHODS //
function getDisplayAndWindowBounds() {
const availWidth = window.screen.availWidth
const availHeight = window.screen.availHeight
// Assume work area starts at (0, 0)
// Work area dimensions approximate available screen area minus some margins
const workArea = {
x: 0, // Typically starts at (0, 0)
y: 0, // Typically starts at (0, 0)
width: availWidth,
height: availHeight,
}
const displayBounds = {
width: window.screen.width,
height: window.screen.height,
availableWidth: window.screen.availWidth,
availableHeight: window.screen.availHeight,
workArea,
}
const windowBounds = {
width: window.innerWidth,
height: window.innerHeight,
}
return { displayBounds, windowBounds }
}
function getPositions() {
const { buttonHeight, buttonWidth } = getButtonSize()
// Get button dimensions and viewport dimensions
const widgetWidth = buttonWidth
const widgetHeight = buttonHeight
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// Calculate center of the viewport
const windowCenterX = viewportWidth / 2
const windowCenterY = viewportHeight / 2
// Calculate button center
const buttonCenterX = widgetWidth / 2
const buttonCenterY = widgetHeight / 2
// Calculate the translation offsets needed to center the button
// Define positions based on work area
const positions = {
'bottom-right': {
x: viewportWidth - widgetWidth - 10,
y: viewportHeight - widgetHeight - 10,
},
'bottom-left': {
x: 10,
y: viewportHeight - widgetHeight - 10,
},
'bottom-center': {
x: (viewportWidth - widgetWidth) / 2,
y: viewportHeight - widgetHeight - 10,
},
'top-right': {
x: viewportWidth - widgetWidth - 10,
y: 10,
},
'top-left': { x: 10, y: 10 },
'top-center': {
x: (viewportWidth - widgetWidth) / 2,
y: 10,
},
'left-center': {
x: 10,
y: windowCenterY - buttonCenterY,
},
'right-center': {
x: viewportWidth - widgetWidth - 10,
y: windowCenterY - buttonCenterY,
},
center: {
x: (viewportWidth - widgetWidth) / 2,
y: windowCenterY - buttonCenterY,
},
}
return positions
}
function getCoordinates(isOutside) {
var position = localStorage.getItem('perf-monitor-position')
if (isOutside) {
var outsidePosition = getNearestPosition()
return outsidePosition
}
const positions = getPositions()
const pos = positions[position]
return pos
}
function getNearestPosition() {
const { buttonHeight, buttonWidth } = getButtonSize()
const widget = document.getElementById('chart-button-container')
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// Get display bounds
const { displayBounds } = getDisplayAndWindowBounds()
// Define positions based on work area
const positions = getPositions()
// Get current window position
const currentX = $(widget).offset().left
let currentY = $(widget).offset().top
const windowCenter = {
x: $(widget).offset().left,
y: $(widget).offset().top,
}
const workAreaCenter = {
x: viewportWidth / 2,
y: viewportHeight / 2,
}
const distanceToCenter = {
x: Math.abs(workAreaCenter.x - windowCenter.x),
y: Math.abs(workAreaCenter.y - windowCenter.y),
}
var threshold = 100 // Define a threshold to determine proximity
const size = localStorage.getItem('chart-size') ?? 'medium'
switch (size) {
case 'small':
threshold = 250
break
case 'medium':
threshold = 200
default:
threshold = 150
break
}
var nearestPosition = ''
if (distanceToCenter.x < threshold && distanceToCenter.y < threshold) {
nearestPosition = 'center'
} else {
// Function to calculate distance
function calculateDistance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
}
// Find the nearest position
let minDistance = Infinity
for (const [key, pos] of Object.entries(positions)) {
// Adjust for edge cases
const adjustedPosX = Math.max(
displayBounds.workArea.x,
Math.min(pos.x, displayBounds.width - buttonWidth),
)
const adjustedPosY = Math.max(
displayBounds.workArea.y,
Math.min(pos.y, displayBounds.height - buttonHeight),
)
const distance = calculateDistance(currentX, currentY, adjustedPosX, adjustedPosY)
if (distance < minDistance) {
minDistance = distance
nearestPosition = key
}
}
}
// Output or use the nearest position
console.log('Nearest position:', nearestPosition)
// Set the position
const pos = positions[nearestPosition]
localStorage.setItem('perf-monitor-position', nearestPosition)
return pos
}
function getButtonSize() {
const size = localStorage.getItem('chart-size') ?? 'medium'
const sizes = getSizes()
const sizeStyles = sizes[size]
var buttonHeight = +sizeStyles.height + 25
const buttonWidth = +sizeStyles.width + 25
var table = document.getElementById('item-table')
var totalRowCount = table.rows.length // 5
var tableHeight = totalRowCount * 30
if (size == 'large') {
buttonHeight = +buttonHeight + tableHeight
}
return { buttonHeight, buttonWidth }
}
async function updateChartSize() {
var table = document.getElementById('item-table')
table.style.display = 'none'
const settingsMenu = document.getElementById('settingsMenu')
settingsMenu.classList.remove('show') // Hide the menu if visible
$('#chart-wrapper').fadeOut()
document.querySelectorAll('.chart-row').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
document.querySelectorAll('canvas').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
const size = localStorage.getItem('chart-size') ?? 'medium'
const chartContainer = document.getElementById('chart-container')
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
chartContainer.classList.remove('small', 'medium', 'large', 'bar', 'line')
chartContainer.classList.add(size)
chartContainer.classList.add(savedChart)
const { buttonHeight, buttonWidth } = getButtonSize()
const chartButtonContainer = document.getElementById('chart-button-container')
var actulaButtonHeight = 0
var actualButtonWidth = 0
var viewportHeight = +buttonHeight
var viewportWidth = +buttonWidth
$(chartButtonContainer).each(function () {
this.style.setProperty('height', `${viewportHeight}px`, 'important')
this.style.setProperty('width', `${viewportWidth}px`, 'important')
})
var position = localStorage.getItem('perf-monitor-position')
if (position === 'center') {
moveToCenter()
} else {
const isOutside = isWindowOutsideWorkingArea()
const pos = getCoordinates(isOutside)
if (pos && isOutside && wasDragged) {
goToPosition(pos)
} else if (pos && !wasDragged) {
goToPosition(pos)
} else {
// do nothing
}
}
var sizeClasses = ['small', 'medium', 'large']
const chartButton = document.getElementById('chart-button')
chartButton.classList.add(size)
sizeClasses.forEach((prop) => {
if (prop != size) {
setTimeout(() => {
chartButton.classList.remove(prop)
}, 500)
}
})
switch (size) {
case 'small':
actulaButtonHeight = viewportHeight * 0.83
actualButtonWidth = viewportWidth * 0.83
break
case 'medium':
actulaButtonHeight = viewportHeight * 0.93
actualButtonWidth = viewportWidth * 0.93
break
default:
actulaButtonHeight = viewportHeight * 0.96
actualButtonWidth = viewportWidth * 0.96
break
}
const bottom = `12.5px`
const right = `12.5px`
$(chartButton).each(function () {
this.style.setProperty('bottom', bottom, 'important')
this.style.setProperty('right', right, 'important')
if (size === 'large') {
this.style.setProperty('background-color', ` #000000d6`, 'important')
} else {
this.style.setProperty('background-color', ` #00000096`, 'important')
}
})
var totalRowCount = table.rows.length // 5
var tableHeight = totalRowCount * 30
var workArea = actulaButtonHeight
if (size == 'large') {
workArea = viewportHeight - tableHeight
}
const hasUpdates = localStorage.getItem('hasUpdates') ?? 'false'
if (hasUpdates === 'true') {
if (savedChart == 'bar') {
$(chartContainer).each(function () {
this.style.setProperty('height', `${workArea * 0.95}px`, 'important')
})
initializeBarChart()
} else {
$(chartContainer).each(function () {
this.style.setProperty('height', `${workArea * 0.87}px`, 'important')
})
setTimeout(() => {
initializeLineChart()
}, 500)
}
} else {
$('#chart-wrapper').fadeIn()
}
localStorage.setItem('hasUpdates', 'false')
const active = `#chart-button.top-left.active`
var positionStyles = {
bottom: bottom,
right: right,
}
var lastClass = {
key: active,
values: [positionStyles],
}
var lastClassString = JSON.stringify(lastClass)
localStorage.setItem('lastClass', lastClassString)
}
const pos = getCoordinates(false)
goToPosition(pos)
var appIsLoaded = false
var shouldShowPerfMonitor = false
if (JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false) {
setTimeout(() => {
// showPerfMonitor()
}, 1500)
}
window.showPerfMonitor = async function () {
shouldShowPerfMonitor = !shouldShowPerfMonitor
localStorage.setItem('shouldShowPerfMonitor', shouldShowPerfMonitor)
const chartButton = document.getElementById('chart-button')
const chartWrapper = document.getElementById('chart-wrapper')
const chartButtonContainer = document.getElementById('chart-button-container')
const resourceMonitorLink = document.getElementById('show_resource_monitor')
if (shouldShowPerfMonitor === true) {
$(chartButtonContainer).toggleClass('active')
localStorage.setItem('hasUpdates', 'true')
await updateChartSize()
$(resourceMonitorLink).fadeOut(500)
appIsLoaded = true
} else {
setTimeout(() => {
$(chartButtonContainer).toggleClass('active')
$(chartButtonContainer).each(function () {
this.style.setProperty('height', `0px`, 'important')
})
}, 500)
chartButton.classList.remove('small', 'medium', 'large')
$(chartWrapper).fadeOut()
$(resourceMonitorLink).fadeIn(500)
}
$(chartButton).toggleClass('active')
}
// when the close button is clicked
document.getElementById('close-button').addEventListener('click', function () {
document.getElementById('settingsMenu').classList.remove('show') // Hide the menu
document.querySelectorAll('.chart-row').forEach((row) => {
row.classList.remove('no-drag')
row.classList.add('drag')
})
showPerfMonitor()
})

View File

@ -0,0 +1,115 @@
export function getResolver(timeout = 5000) {
const resolver = {}
resolver.id = generateId(8)
resolver.completed = false
resolver.resolved = false
resolver.rejected = false
resolver.promise = new Promise((resolve, reject) => {
resolver.reject = () => {
resolver.completed = true
resolver.rejected = true
reject()
}
resolver.resolve = (data) => {
resolver.completed = true
resolver.resolved = true
resolve(data)
}
})
resolver.timeout = setTimeout(() => {
if (!resolver.completed) {
resolver.reject()
}
}, timeout)
return resolver
}
const DEBOUNCE_FN_TO_PROMISE = new WeakMap()
export function debounce(fn, ms = 64) {
if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) {
DEBOUNCE_FN_TO_PROMISE.set(
fn,
wait(ms).then(() => {
DEBOUNCE_FN_TO_PROMISE.delete(fn)
fn()
}),
)
}
return DEBOUNCE_FN_TO_PROMISE.get(fn)
}
export function wait(ms = 16) {
if (ms === 16) {
return new Promise((resolve) => {
requestAnimationFrame(() => {
resolve()
})
})
}
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, ms)
})
}
function dec2hex(dec) {
return dec.toString(16).padStart(2, '0')
}
export function generateId(length) {
const arr = new Uint8Array(length / 2)
crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
export function getObjectValue(obj, objKey, def) {
if (!obj || !objKey) return def
const keys = objKey.split('.')
const key = keys.shift()
const found = obj[key]
if (keys.length) {
return getObjectValue(found, keys.join('.'), def)
}
return found
}
export function setObjectValue(obj, objKey, value, createMissingObjects = true) {
if (!obj || !objKey) return obj
const keys = objKey.split('.')
const key = keys.shift()
if (obj[key] === undefined) {
if (!createMissingObjects) {
return
}
obj[key] = {}
}
if (!keys.length) {
obj[key] = value
} else {
if (typeof obj[key] != 'object') {
obj[key] = {}
}
setObjectValue(obj[key], keys.join('.'), value, createMissingObjects)
}
return obj
}
export function moveArrayItem(arr, itemOrFrom, to) {
const from = typeof itemOrFrom === 'number' ? itemOrFrom : arr.indexOf(itemOrFrom)
arr.splice(to, 0, arr.splice(from, 1)[0])
}
export function removeArrayItem(arr, itemOrIndex) {
const index = typeof itemOrIndex === 'number' ? itemOrIndex : arr.indexOf(itemOrIndex)
arr.splice(index, 1)
}
export function injectCss(href) {
if (document.querySelector(`link[href^="${href}"]`)) {
return Promise.resolve()
}
return new Promise((resolve) => {
const link = document.createElement('link')
link.setAttribute('rel', 'stylesheet')
link.setAttribute('type', 'text/css')
const timeout = setTimeout(resolve, 1000)
link.addEventListener('load', (e) => {
clearInterval(timeout)
resolve()
})
link.href = href
document.head.appendChild(link)
})
}

7
web/assets/js/socket.io.min.js vendored Normal file

File diff suppressed because one or more lines are too long

61
web/templates/index.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>System Monitor</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.data-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.data-item {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>Real-Time System Monitor</h1>
<div class="data-container" id="data-container">
<div class="data-item" id="cpu">CPU: Loading...</div>
<div class="data-item" id="ram">RAM: Loading...</div>
<div class="data-item" id="gpu">GPU: Loading...</div>
<div class="data-item" id="vram">VRAM: Loading...</div>
<div class="data-item" id="hdd">HDD: Loading...</div>
<div class="data-item" id="temp">Temperature: Loading...</div>
</div>
<script>
const socket = io('http://localhost:5000')
// Listen for 'data_update' event and update the data on the page
socket.on('data_update', (data) => {
document.getElementById('cpu').textContent = `CPU: ${data.cpu.toFixed(2)}%`
document.getElementById('ram').textContent = `RAM: ${data.ram.toFixed(2)}%`
document.getElementById('gpu').textContent = `GPU: ${data.gpu.toFixed(2)}%`
document.getElementById('vram').textContent = `VRAM: ${data.vram.toFixed(2)}%`
document.getElementById('hdd').textContent = `HDD: ${data.hdd.toFixed(2)}%`
document.getElementById('temp').textContent = `Temperature: ${data.temp.toFixed(2)}°C`
})
socket.on('connect', () => {
console.log('Connected to the server')
})
socket.on('disconnect', () => {
console.log('Disconnected from the server')
})
</script>
</body>
</html>

View File

@ -0,0 +1,9 @@
<div id="perf-monitor-container"></div>
<img
src="/file=web/assets/img/clearfix.png"
onload="{
var script = document.createElement('script');
script.src = '/file=web/assets/js/dependencies.js';
document.body.appendChild(script);
}"
/>

View File

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<body>
<elegant-resource-monitor></elegant-resource-monitor>
</body>
</html>

735
webui.py

File diff suppressed because it is too large Load Diff