From a8dceb0d7b8ae63e55b829f7d5c85eca2834f71c Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 17 May 2024 21:51:25 -0400 Subject: [PATCH] Add in safetensors metadata (trained words, base model file). For #223 --- py/rgthree_server.py | 25 +- py/utils_info.py | 405 +++++++++++++----- py/utils_userdata.py | 7 + src_web/comfyui/dialog_info.ts | 188 ++++++--- src_web/comfyui/rgthree.ts | 11 + src_web/common/css/dialog.scss | 402 ------------------ src_web/common/css/dialog_model_info.scss | 475 ++++++++++++++++++++++ src_web/typings/rgthree.d.ts | 7 + web/comfyui/dialog_info.js | 177 +++++--- web/comfyui/rgthree.css | 353 ---------------- web/comfyui/rgthree.js | 9 + web/common/css/dialog.css | 353 ---------------- web/common/css/dialog_model_info.css | 417 +++++++++++++++++++ 13 files changed, 1492 insertions(+), 1337 deletions(-) create mode 100644 src_web/common/css/dialog_model_info.scss create mode 100644 web/common/css/dialog_model_info.css diff --git a/py/rgthree_server.py b/py/rgthree_server.py index 506850f..bb4c244 100644 --- a/py/rgthree_server.py +++ b/py/rgthree_server.py @@ -8,8 +8,7 @@ import asyncio from datetime import datetime from .utils_server import get_param -from .utils import get_dict_value, load_json_file, is_dict_value_falsy, save_json_file -from .utils_info import get_model_info, get_sha256_hash, set_model_info_partial +from .utils_info import delete_all_model_info, get_model_info, set_model_info_partial from server import PromptServer from aiohttp import web @@ -104,18 +103,33 @@ async def api_get_loras(request): @routes.get('/rgthree/api/loras/info') async def api_get_loras_info(request): """ Returns a list loras info; either all or a single if provided a 'file' param. """ - api_response = await get_loras_info_response(request) + api_response = await get_loras_info_response(request, maybe_fetch_metadata=True) + return web.json_response(api_response) + + +@routes.get('/rgthree/api/loras/info/clear') +async def delete_lora_info(request): + """Clears lora info from the filesystem for the provided file.""" + api_response = {'status': 200} + lora_file = get_param(request, 'file') + if lora_file is None: + api_response['status'] = '404' + api_response['error'] = 'No Lora found at path' + else: + await delete_all_model_info(lora_file) return web.json_response(api_response) @routes.get('/rgthree/api/loras/info/refresh') async def refresh_get_loras_info(request): """ Refreshes lora info; either all or a single if provided a 'file' param. """ - api_response = await get_loras_info_response(request, maybe_fetch_civitai=True) + api_response = await get_loras_info_response(request, + maybe_fetch_civitai=True, + maybe_fetch_metadata=True) return web.json_response(api_response) -async def get_loras_info_response(request, maybe_fetch_civitai=False): +async def get_loras_info_response(request, maybe_fetch_civitai=False, maybe_fetch_metadata=False): """Gets lora info for all or a single lora""" api_response = {'status': 200} lora_file = get_param(request, 'file') @@ -124,6 +138,7 @@ async def get_loras_info_response(request, maybe_fetch_civitai=False): if lora_file is not None: info_data = await get_model_info(lora_file, maybe_fetch_civitai=maybe_fetch_civitai, + maybe_fetch_metadata=maybe_fetch_metadata, abandon_if_no_file=light) if info_data is None: api_response['status'] = '404' diff --git a/py/utils_info.py b/py/utils_info.py index fa7f57a..2c2de38 100644 --- a/py/utils_info.py +++ b/py/utils_info.py @@ -8,24 +8,39 @@ import copy from datetime import datetime -from .utils import get_dict_value, is_dict_value_falsy, load_json_file, save_json_file -from .utils_userdata import read_userdata_json, save_userdata_json +from .utils import get_dict_value, load_json_file, save_json_file +from .utils_userdata import read_userdata_json, save_userdata_json, delete_userdata_file import folder_paths from server import PromptServer +async def delete_all_model_info(file: str, model_type="loras"): + """Delete the info json, and the civitai & metadata caches.""" + file_path = get_folder_path(file, model_type) + if file_path is None: + return + try_info_path = f'{file_path}.rgthree-info.json' + if os.path.isfile(try_info_path): + os.remove(try_info_path) + file_hash = _get_sha256_hash(file_path) + json_file_path = f'civitai/{file_hash}.json' + delete_userdata_file(json_file_path) + json_file_path = f'metadata/{file_hash}.json' + delete_userdata_file(json_file_path) + + async def get_model_info(file: str, model_type="loras", default=None, maybe_fetch_civitai=False, force_fetch_civitai=False, + maybe_fetch_metadata=False, + force_fetch_metadata=False, abandon_if_no_file=False): - file_path = folder_paths.get_full_path(model_type, file) - if not os.path.exists(file_path): - file_path = os.path.abspath(file_path) - if not os.path.exists(file_path): + file_path = get_folder_path(file, model_type) + if file_path is None: return default info_data = {} @@ -67,136 +82,226 @@ async def get_model_info(file: str, info_data['raw'] = {} should_save = True - if force_fetch_civitai is True or (maybe_fetch_civitai is True and - 'civitai' not in info_data['raw']): - data_civitai = get_model_civitai_data(file, - model_type=model_type, - default={}, - refresh=force_fetch_civitai) + should_save = _update_data(info_data) or should_save - if 'name' not in info_data: - info_data['name'] = get_dict_value(data_civitai, 'model.name', '') + should_fetch_civitai = force_fetch_civitai is True or (maybe_fetch_civitai is True and + 'civitai' not in info_data['raw']) + should_fetch_metadata = force_fetch_metadata is True or (maybe_fetch_metadata is True and + 'metadata' not in info_data['raw']) + + if should_fetch_metadata: + data_meta = _get_model_metadata(file, + model_type=model_type, + default={}, + refresh=force_fetch_metadata) + should_save = _merge_metadata(info_data, data_meta) or should_save + + if should_fetch_civitai: + data_civitai = _get_model_civitai_data(file, + model_type=model_type, + default={}, + refresh=force_fetch_civitai) + should_save = _merge_civitai_data(info_data, data_civitai) or should_save + + if 'sha256' not in info_data: + file_hash = _get_sha256_hash(file_path) + if file_hash is not None: + info_data['sha256'] = file_hash should_save = True - version_name = get_dict_value(data_civitai, 'name') - if version_name is not None: - info_data['name'] += f' - {version_name}' - - if 'type' not in info_data: - info_data['type'] = get_dict_value(data_civitai, 'model.type') - should_save = True - if 'baseModel' not in info_data: - info_data['baseModel'] = get_dict_value(data_civitai, 'baseModel') - should_save = True - - if 'triggerWords' not in info_data: - if 'triggerWords' in data_civitai: - info_data['triggerWords'] = get_dict_value(data_civitai, 'triggerWords') - should_save = True - if is_dict_value_falsy(data_civitai, 'triggerWords') and 'trainedWords' in data_civitai: - info_data['triggerWords'] = get_dict_value(data_civitai, 'trainedWords') - if not is_dict_value_falsy(data_civitai, - 'triggerWords') and ',' in info_data['triggerWords'][0]: - trigger_words = re.sub(r"^,", "", info_data['triggerWords'][0]) - trigger_words = re.sub(r",$", "", trigger_words) - trigger_words = re.sub(r"\s*,\s*", ",", trigger_words) - info_data['triggerWords'] = trigger_words.split(',') - should_save = True - - if 'sha256' not in info_data: - info_data['sha256'] = data_civitai['_sha256'] - should_save = True - - if 'modelId' in data_civitai: - info_data['links'] = info_data['links'] if 'links' in info_data else [] - civitai_link = f'https://civitai.com/models/{get_dict_value(data_civitai, "modelId")}' - if get_dict_value(data_civitai, "id"): - civitai_link += f'?modelVersionId={get_dict_value(data_civitai, "id")}' - info_data['links'].append(civitai_link) - info_data['links'].append(data_civitai['_civitai_api']) - should_save = True - - # Take images from civitai - if 'images' in data_civitai: - info_data_image_urls = list( - map(lambda i: i['url'] if 'url' in i else None, info_data['images'])) - for img in data_civitai['images']: - img_url = get_dict_value(img, 'url') - if img_url is not None and img_url not in info_data_image_urls: - img_id = os.path.splitext(os.path.basename(img_url))[0] if img_url is not None else None - img_data = { - 'url': img_url, - 'civitaiUrl': f'https://civitai.com/images/{img_id}' if img_id is not None else None, - 'width': get_dict_value(img, 'width'), - 'height': get_dict_value(img, 'height'), - 'type': get_dict_value(img, 'type'), - 'nsfwLevel': get_dict_value(img, 'nsfwLevel'), - 'seed': get_dict_value(img, 'meta.seed'), - 'positive': get_dict_value(img, 'meta.prompt'), - 'negative': get_dict_value(img, 'meta.negativePrompt'), - 'steps': get_dict_value(img, 'meta.steps'), - 'sampler': get_dict_value(img, 'meta.sampler'), - 'cfg': get_dict_value(img, 'meta.cfgScale'), - 'model': get_dict_value(img, 'meta.Model'), - 'resources': get_dict_value(img, 'meta.resources'), - } - info_data['images'].append(img_data) - should_save = True - - # The raw data - if 'civitai' not in info_data['raw']: - info_data['raw']['civitai'] = data_civitai - should_save = True - - else: - if 'sha256' not in info_data: - file_hash = get_sha256_hash(file_path) - if file_hash is not None: - info_data['sha256'] = file_hash - should_save = True # If we've fetched civitai, then the UI is likely waiting to see if the refreshed data is coming # in. Not needed yet, only for Lora Chooser. # await PromptServer.instance.send("rgthree-lora-info", {"data": info_data}) if should_save: + if 'trainedWords' in info_data: + # Sort by count; if it doesn't exist, then assume it's a top item from civitai or elsewhere. + info_data['trainedWords'] = sorted(info_data['trainedWords'], + key=lambda w: w['count'] if 'count' in w else 99999, + reverse=True) save_model_info(file, info_data, model_type=model_type) + + return info_data -def get_sha256_hash(path: str): - """Returns the hash for the file.""" - file_hash = None - sha256_hash = hashlib.sha256() - with open(path, "rb") as f: - # Read and update hash string value in blocks of 4K - for byte_block in iter(lambda: f.read(4096), b""): - sha256_hash.update(byte_block) - file_hash = sha256_hash.hexdigest() - return file_hash +def _update_data(info_data: dict) -> bool: + """Ports old data to new data if necessary.""" + should_save = False + # If we have "triggerWords" then move them over to "trainedWords" + if 'triggerWords' in info_data and len(info_data['triggerWords']) > 0: + civitai_words = ','.join((get_dict_value(info_data, 'raw.civitai.triggerWords', default=[]) + + get_dict_value(info_data, 'raw.civitai.trainedWords', default=[]))) + if 'trainedWords' not in info_data: + info_data['trainedWords'] = [] + for trigger_word in info_data['triggerWords']: + word_data = next((data for data in info_data['trainedWords'] if data['word'] == trigger_word), + None) + if word_data is None: + word_data = {'word': trigger_word} + info_data['trainedWords'].append(word_data) + if trigger_word in civitai_words: + word_data['civitai'] = True + else: + word_data['user'] = True + + del info_data['triggerWords'] + should_save = True + return should_save -def get_model_civitai_data(file: str, model_type="loras", default=None, refresh=False): +def _merge_metadata(info_data: dict, data_meta: dict) -> bool: + """Returns true if data was saved.""" + should_save = False + + base_model_file = get_dict_value(data_meta, 'ss_sd_model_name', None) + if base_model_file: + info_data['baseModelFile'] = base_model_file + + # Loop over metadata tags + trained_words = {} + if 'ss_tag_frequency' in data_meta and isinstance(data_meta['ss_tag_frequency'], dict): + for bucket_value in data_meta['ss_tag_frequency'].values(): + if isinstance(bucket_value, dict): + for tag, count in bucket_value.items(): + if tag not in trained_words: + trained_words[tag] = {'word': tag, 'count': 0, 'metadata': True} + trained_words[tag]['count'] = trained_words[tag]['count'] + count + + if 'trainedWords' not in info_data: + info_data['trainedWords'] = list(trained_words.values()) + should_save = True + else: + # We can't merge, because the list may have other data, like it's part of civitaidata. + merged_dict = {} + for existing_word_data in info_data['trainedWords']: + merged_dict[existing_word_data['word']] = existing_word_data + for new_key, new_word_data in trained_words.items(): + if new_key not in merged_dict: + merged_dict[new_key] = {} + merged_dict[new_key] = {**merged_dict[new_key], **new_word_data} + info_data['trainedWords'] = list(merged_dict.values()) + should_save = True + + # trained_words = list(trained_words.values()) + # info_data['meta_trained_words'] = trained_words + info_data['raw']['metadata'] = data_meta + should_save = True + + if 'sha256' not in info_data and '_sha256' in data_meta: + info_data['sha256'] = data_meta['_sha256'] + should_save = True + + return should_save + + +def _merge_civitai_data(info_data: dict, data_civitai: dict) -> bool: + """Returns true if data was saved.""" + should_save = False + + if 'name' not in info_data: + info_data['name'] = get_dict_value(data_civitai, 'model.name', '') + should_save = True + version_name = get_dict_value(data_civitai, 'name') + if version_name is not None: + info_data['name'] += f' - {version_name}' + + if 'type' not in info_data: + info_data['type'] = get_dict_value(data_civitai, 'model.type') + should_save = True + if 'baseModel' not in info_data: + info_data['baseModel'] = get_dict_value(data_civitai, 'baseModel') + should_save = True + + # We always want to merge triggerword. + civitai_trigger = get_dict_value(data_civitai, 'triggerWords', default=[]) + civitai_trained = get_dict_value(data_civitai, 'trainedWords', default=[]) + civitai_words = ','.join(civitai_trigger + civitai_trained) + if civitai_words: + civitai_words = re.sub(r"\s*,\s*", ",", civitai_words) + civitai_words = re.sub(r",+", ",", civitai_words) + civitai_words = re.sub(r"^,", "", civitai_words) + civitai_words = re.sub(r",$", "", civitai_words) + if civitai_words: + civitai_words = civitai_words.split(',') + if 'trainedWords' not in info_data: + info_data['trainedWords'] = [] + for trigger_word in civitai_words: + word_data = next( + (data for data in info_data['trainedWords'] if data['word'] == trigger_word), None) + if word_data is None: + word_data = {'word': trigger_word} + info_data['trainedWords'].append(word_data) + word_data['civitai'] = True + + if 'sha256' not in info_data: + info_data['sha256'] = data_civitai['_sha256'] + should_save = True + + if 'modelId' in data_civitai: + info_data['links'] = info_data['links'] if 'links' in info_data else [] + civitai_link = f'https://civitai.com/models/{get_dict_value(data_civitai, "modelId")}' + if get_dict_value(data_civitai, "id"): + civitai_link += f'?modelVersionId={get_dict_value(data_civitai, "id")}' + info_data['links'].append(civitai_link) + info_data['links'].append(data_civitai['_civitai_api']) + should_save = True + + # Take images from civitai + if 'images' in data_civitai: + info_data_image_urls = list(map(lambda i: i['url'] + if 'url' in i else None, info_data['images'])) + for img in data_civitai['images']: + img_url = get_dict_value(img, 'url') + if img_url is not None and img_url not in info_data_image_urls: + img_id = os.path.splitext(os.path.basename(img_url))[0] if img_url is not None else None + img_data = { + 'url': img_url, + 'civitaiUrl': f'https://civitai.com/images/{img_id}' if img_id is not None else None, + 'width': get_dict_value(img, 'width'), + 'height': get_dict_value(img, 'height'), + 'type': get_dict_value(img, 'type'), + 'nsfwLevel': get_dict_value(img, 'nsfwLevel'), + 'seed': get_dict_value(img, 'meta.seed'), + 'positive': get_dict_value(img, 'meta.prompt'), + 'negative': get_dict_value(img, 'meta.negativePrompt'), + 'steps': get_dict_value(img, 'meta.steps'), + 'sampler': get_dict_value(img, 'meta.sampler'), + 'cfg': get_dict_value(img, 'meta.cfgScale'), + 'model': get_dict_value(img, 'meta.Model'), + 'resources': get_dict_value(img, 'meta.resources'), + } + info_data['images'].append(img_data) + should_save = True + + # The raw data + if 'civitai' not in info_data['raw']: + info_data['raw']['civitai'] = data_civitai + should_save = True + + return should_save + + +def _get_model_civitai_data(file: str, model_type="loras", default=None, refresh=False): """Gets the civitai data, either cached from the user directory, or from civitai api.""" - file_path = folder_paths.get_full_path(model_type, file) - if not os.path.exists(file_path): - file_path = os.path.abspath(file_path) - - file_hash = get_sha256_hash(file_path) + file_hash = _get_sha256_hash(get_folder_path(file, model_type)) if file_hash is None: return None + json_file_path = f'civitai/{file_hash}.json' + api_url = f'https://civitai.com/api/v1/model-versions/by-hash/{file_hash}' - file_data = read_userdata_json(f'info/{file_hash}_civitai.json') - if file_data is None or refresh == True: + file_data = read_userdata_json(json_file_path) + if file_data is None or refresh is True: try: response = requests.get(api_url, timeout=5000) data = response.json() - save_userdata_json(f'civitai/{file_hash}.json', { + save_userdata_json(json_file_path, { 'url': api_url, 'timestamp': datetime.now().timestamp(), 'response': data }) - file_data = read_userdata_json(f'civitai/{file_hash}.json') + file_data = read_userdata_json(json_file_path) except requests.exceptions.RequestException as e: # This is the correct syntax print(e) response = file_data['response'] if file_data is not None and 'response' in file_data else None @@ -206,6 +311,86 @@ def get_model_civitai_data(file: str, model_type="loras", default=None, refresh= return response if response is not None else default +def _get_model_metadata(file: str, model_type="loras", default=None, refresh=False): + """Gets the metadata from the file itself.""" + file_path = get_folder_path(file, model_type) + file_hash = _get_sha256_hash(file_path) + if file_hash is None: + return default + + json_file_path = f'metadata/{file_hash}.json' + + file_data = read_userdata_json(json_file_path) + if file_data is None or refresh is True: + data = _read_file_metadata_from_header(file_path) + if data is not None: + file_data = {'url': file, 'timestamp': datetime.now().timestamp(), 'response': data} + save_userdata_json(json_file_path, file_data) + response = file_data['response'] if file_data is not None and 'response' in file_data else None + if response is not None: + response['_sha256'] = file_hash + return response if response is not None else default + + +def _read_file_metadata_from_header(file_path: str) -> dict: + """Reads the file's header and returns a JSON dict metdata if available.""" + data = None + try: + if file_path.endswith('.safetensors'): + with open(file_path, "rb") as file: + # https://github.com/huggingface/safetensors#format + # 8 bytes: N, an unsigned little-endian 64-bit integer, containing the size of the header + header_size = int.from_bytes(file.read(8), "little", signed=False) + + if header_size <= 0: + raise BufferError("Invalid header size") + + header = file.read(header_size) + if header is None: + raise BufferError("Invalid header") + + header_json = json.loads(header) + data = header_json["__metadata__"] if "__metadata__" in header_json else None + + if data is not None: + for key, value in data.items(): + if isinstance(value, str) and value.startswith('{') and value.endswith('}'): + try: + value_as_json = json.loads(value) + data[key] = value_as_json + except Exception: + print(f'metdata for field {key} did not parse as json') + except requests.exceptions.RequestException as e: + print(e) + data = None + + return data + + +def get_folder_path(file: str, model_type="loras"): + """Gets the file path ensuring it exists.""" + file_path = folder_paths.get_full_path(model_type, file) + if not os.path.exists(file_path): + file_path = os.path.abspath(file_path) + if not os.path.exists(file_path): + return None + return file_path + + +def _get_sha256_hash(file_path: str): + """Returns the hash for the file.""" + if not file_path or not os.path.exists(file_path): + return None + file_hash = None + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + # Read and update hash string value in blocks of 4K + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + file_hash = sha256_hash.hexdigest() + return file_hash + + async def set_model_info_partial(file: str, info_data_partial, model_type="loras"): """Sets partial data into the existing model info data.""" info_data = await get_model_info(file, model_type=model_type, default={}) diff --git a/py/utils_userdata.py b/py/utils_userdata.py index 5b32b99..1a1482f 100644 --- a/py/utils_userdata.py +++ b/py/utils_userdata.py @@ -22,6 +22,13 @@ def save_userdata_file(rel_path: str, content: str): file.write(content) +def delete_userdata_file(rel_path: str): + """Deletes a file from the userdata directory.""" + file_path = clean_path(rel_path) + if os.path.isfile(file_path): + os.remove(file_path) + + def read_userdata_json(rel_path: str): """Reads a json file from the userdata directory.""" file_path = clean_path(rel_path) diff --git a/src_web/comfyui/dialog_info.ts b/src_web/comfyui/dialog_info.ts index b7b9f9d..0762b5b 100644 --- a/src_web/comfyui/dialog_info.ts +++ b/src_web/comfyui/dialog_info.ts @@ -5,18 +5,30 @@ import { appendChildren, getClosestOrSelf, queryOne, - setAttributes, + query, } from "rgthree/common/utils_dom.js"; -import { - logoCivitai, - link, - pencilColored, - diskColored, - dotdotdot, -} from "rgthree/common/media/svgs.js"; -import { rgthreeApi } from "rgthree/common/rgthree_api.js"; +import { logoCivitai, link, pencilColored, diskColored } from "rgthree/common/media/svgs.js"; import { RgthreeModelInfo } from "typings/rgthree.js"; import { SERVICE as MODEL_INFO_SERVICE } from "rgthree/common/model_info_service.js"; +import { rgthree } from "./rgthree.js"; +import { generateId } from "rgthree/common/shared_utils.js"; + +function injectCss(): Promise { + const href = "rgthree/common/css/dialog_model_info.css"; + if (queryOne(`link[href^="${href}"]`)) { + return Promise.resolve(); + } + return new Promise((resolve) => { + const link = $el('link[rel="stylesheet"][type="text/css"]'); + const timeout = setTimeout(resolve, 1000); + link.addEventListener("load", (e) => { + clearInterval(timeout); + resolve(); + }); + link.href = href; + document.head.appendChild(link); + }); +} /** * A dialog that displays information about a model/lora/etc. @@ -39,7 +51,9 @@ export class RgthreeInfoDialog extends RgthreeDialog { } private async init(file: string) { + const cssPromise = injectCss(); this.modelInfo = await MODEL_INFO_SERVICE.getLora(file, false, false); + await cssPromise; this.setContent(this.getInfoContent()); this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown"); this.attachEvents(); @@ -54,53 +68,86 @@ export class RgthreeInfoDialog extends RgthreeDialog { private attachEvents() { this.contentElement.addEventListener("click", async (e: MouseEvent) => { - const info = this.modelInfo; - if (!info?.file) { - return; - } - const target = getClosestOrSelf(e.target as HTMLElement, "[data-action]"); const action = target?.getAttribute("data-action"); - if (action === "fetch-civitai") { - this.modelInfo = await MODEL_INFO_SERVICE.refreshLora(info.file); - this.setContent(this.getInfoContent()); - this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown"); - } else if (action === "edit-row") { - const tr = target!.closest("tr")!; - const td = queryOne("td:nth-child(2)", tr)!; - const input = td.querySelector("input,textarea"); - if (!input) { - const fieldName = tr.dataset["fieldName"] as string; - tr.classList.add("-rgthree-editing"); - const isTextarea = fieldName === "userNote"; - const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, { - value: td.textContent, - }); - input.addEventListener("keydown", (e) => { - if (!isTextarea && e.key === "Enter") { - const modified = saveEditableRow(info!, tr, true); - this.modifiedModelData = this.modifiedModelData || modified; - e.stopPropagation(); - e.preventDefault(); - } else if (e.key === "Escape") { - const modified = saveEditableRow(info!, tr, false); - this.modifiedModelData = this.modifiedModelData || modified; - e.stopPropagation(); - e.preventDefault(); - } - }); - appendChildren(empty(td), [input]); - input.focus(); - } else if (target!.nodeName.toLowerCase() === "button") { - const modified = saveEditableRow(info!, tr, true); - this.modifiedModelData = this.modifiedModelData || modified; - } - e.preventDefault(); - e.stopPropagation(); + if (!target || !action) { + return; } + await this.handleEventAction(action, target, e); }); } + private async handleEventAction(action: string, target: HTMLElement, e?: Event) { + const info = this.modelInfo!; + if (!info?.file) { + return; + } + if (action === "fetch-civitai") { + this.modelInfo = await MODEL_INFO_SERVICE.refreshLora(info.file); + this.setContent(this.getInfoContent()); + this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown"); + } else if (action === "copy-trained-words") { + const selected = query(".-rgthree-is-selected", target.closest("tr")!); + const text = selected.map((el) => el.getAttribute("data-word")).join(", "); + await navigator.clipboard.writeText(text); + rgthree.showMessage({ + id: "copy-trained-words-" + generateId(4), + type: "success", + message: `Successfully copied ${selected.length} key word${ + selected.length === 1 ? "" : "s" + }.`, + timeout: 4000, + }); + } else if (action === "toggle-trained-word") { + target?.classList.toggle("-rgthree-is-selected"); + const tr = target.closest("tr"); + if (tr) { + const span = queryOne("td:first-child > *", tr)!; + let small = queryOne("small", span); + if (!small) { + small = $el("small", { parent: span }); + } + const num = query(".-rgthree-is-selected", tr).length; + small.innerHTML = num + ? `${num} selected | Copy` + : ""; + // this.handleEventAction('copy-trained-words', target, e); + } + } else if (action === "edit-row") { + const tr = target!.closest("tr")!; + const td = queryOne("td:nth-child(2)", tr)!; + const input = td.querySelector("input,textarea"); + if (!input) { + const fieldName = tr.dataset["fieldName"] as string; + tr.classList.add("-rgthree-editing"); + const isTextarea = fieldName === "userNote"; + const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, { + value: td.textContent, + }); + input.addEventListener("keydown", (e) => { + if (!isTextarea && e.key === "Enter") { + const modified = saveEditableRow(info!, tr, true); + this.modifiedModelData = this.modifiedModelData || modified; + e.stopPropagation(); + e.preventDefault(); + } else if (e.key === "Escape") { + const modified = saveEditableRow(info!, tr, false); + this.modifiedModelData = this.modifiedModelData || modified; + e.stopPropagation(); + e.preventDefault(); + } + }); + appendChildren(empty(td), [input]); + input.focus(); + } else if (target!.nodeName.toLowerCase() === "button") { + const modified = saveEditableRow(info!, tr, true); + this.modifiedModelData = this.modifiedModelData || modified; + } + e?.preventDefault(); + e?.stopPropagation(); + } + } + private getInfoContent() { const info = this.modelInfo || {}; const civitaiLink = info.links?.find((i) => i.includes("civitai.com/models")); @@ -145,14 +192,21 @@ export class RgthreeInfoDialog extends RgthreeDialog { ) : "" } - ${infoTableRow("Base Model", info.baseModel || "")} - - ${infoTableRow("Name", info.name || "", "The name for display.", "name")} ${infoTableRow( - "Trigger Words", - info.triggerWords?.join(", ") ?? "", - "Easily keep track of the trigger words to reference here.", - "triggerWords", + "Base Model", + (info.baseModel || " ") + (info.baseModelFile ? `(${info.baseModelFile})` : ""), + )} + ${infoTableRow( + "Name", + info.name || info.raw?.metadata?.ss_output_name || "", + "The name for display.", + "name", + )} + + ${infoTableRow( + "Trained Words", + getTrainedWordsMarkup(info.trainedWords) ?? "", + "Trained words from the metadata and/or civitai. Click to select for copy.", )} ${infoTableRow( "Strength Min", @@ -241,7 +295,9 @@ function infoTableRow( editableFieldName ? `data-field-name="${editableFieldName}"` : "" }> ${name} ${help ? `` : ""} - ${value} + ${ + String(value).startsWith("<") ? value : `${value}` + } ${ editableFieldName ? `` @@ -250,6 +306,22 @@ function infoTableRow( `; } +function getTrainedWordsMarkup(words: RgthreeModelInfo["trainedWords"]) { + let markup = `
    `; + for (const wordData of words || []) { + markup += `
  • + ${wordData.word} + ${wordData.civitai ? logoCivitai : ""} + ${wordData.count != null ? `${wordData.count}` : ""} +
  • `; + } + markup += `
`; + console.log(markup); + return markup; +} + /** * Saves / cancels an editable row. Returns a boolean if the data was modified. */ diff --git a/src_web/comfyui/rgthree.ts b/src_web/comfyui/rgthree.ts index 91fbefb..ba14f46 100644 --- a/src_web/comfyui/rgthree.ts +++ b/src_web/comfyui/rgthree.ts @@ -23,6 +23,7 @@ import { RgthreeProgressBar } from "rgthree/common/progress_bar.js"; import { RgthreeConfigDialog } from "./config.js"; import { iconGear, iconReplace, iconStarFilled, logoRgthree } from "rgthree/common/media/svgs.js"; import type { Bookmark } from "./bookmark"; +import { query } from "rgthree/common/utils_dom.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphCanvas: typeof TLGraphCanvas; @@ -789,6 +790,16 @@ class Rgthree extends EventTarget { container.classList.add("rgthree-top-messages-container"); document.body.appendChild(container); } + // If we have a dialog open then we want to append the message to the dialog so they show over + // the modal. + const dialogs = query('dialog[open]'); + if (dialogs.length) { + let dialog = dialogs[dialogs.length - 1]!; + dialog.appendChild(container); + dialog.addEventListener('close', (e) => { + document.body.appendChild(container!); + }); + } // Hide if we exist. await this.hideMessage(data.id); diff --git a/src_web/common/css/dialog.scss b/src_web/common/css/dialog.scss index d0d4edf..c47e2f9 100644 --- a/src_web/common/css/dialog.scss +++ b/src_web/common/css/dialog.scss @@ -127,405 +127,3 @@ body.rgthree-dialog-open > *:not(.rgthree-dialog):not(.rgthree-top-messages-cont filter: blur(5px); } - -.rgthree-info-dialog { - - min-width: 600px; - - .rgthree-info-tags { - list-style: none; - padding: 0; - margin: 0; - display: flex; - - > li { - display: inline-flex; - margin: 0; - vertical-align: top; - - + li { - margin-left: 6px; - } - &:not(.-link) + li.-link { - margin-left: auto; - } - - > * { - border-radius: 4px; - line-height: 1; - color: rgba(255,255,255,0.85); - background: rgb(69, 92, 85);; - font-size: 14px; - font-weight: bold; - text-decoration: none; - display: flex; - height: 1.6em; - padding-left: .5em; - padding-right: .5em; - padding-bottom: .1em; - align-content: center; - justify-content: center; - align-items: center; - box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); - - > svg { - width: 16px; - height: 16px; - - &:first-child { - margin-right: .3em; - display: none; - } - &:last-child { - margin-left: .5em; - fill: currentColor; - } - } - - - &[href] { - box-shadow: inset 0px 1px 0px rgba(255,255,255,0.25), inset 0px -1px 0px rgba(0,0,0,0.66); - } - - &:empty { - display: none; - } - } - - &.-civitai > * { - color: #ddd; - background: #1b65aa; - transition: all 0.15s ease-in-out; - &:hover { - color: #fff; - border-color: #1971c2; - background: #1971c2; - } - } - &.-type > * { - background: rgb(73, 54, 94); - color: rgb(228, 209, 248); - } - } - } - - .rgthree-info-table { - border-collapse: collapse; - margin: 16px 0px; - width: 100%; - font-size: 12px; - - tr.editable button { - display: flex; - width: 28px; - height: 28px; - align-items: center; - justify-content: center; - - svg + svg {display: none;} - } - tr.editable.-rgthree-editing button { - svg {display: none;} - svg + svg {display: inline-block;} - } - - td { - position: relative; - border: 1px solid rgba(255,255,255,0.25); - padding: 0; - - &:first-child { - background: rgba(255,255,255,0.075); - width: 10px; // Small, so it doesn't adjust. - > *:first-child { - white-space: nowrap; - padding-right: 32px; - } - } - - a, a:hover, a:visited { - color: inherit; - } - - ul { - margin: .5em 0; - padding: 0 0 0 1em; - } - - svg { - width: 1.3333em; - height: 1.3333em; - vertical-align: -0.285em; - - &.logo-civitai { - margin-right: 0.3333em; - } - } - - > *:first-child { - display: block; - padding: 6px 10px; - } - > input, > textarea{ - padding: 5px 10px; - border: 0; - box-shadow: inset 1px 1px 5px 0px rgba(0,0,0,0.5); - font: inherit; - appearance: none; - background: #fff; - color: #121212; - resize: vertical; - - &:only-child { - width: 100%; - } - } - } - - tr[data-field-name="userNote"] td > span:first-child { - white-space: pre; - } - - tr.rgthree-info-table-break-row td { - border: 0; - background: transparent; - padding: 12px 4px 4px; - font-size: 1.2em; - - > small { - font-style: italic; - opacity: 0.66; - } - - &:empty { - padding: 4px; - } - } - - td .-help { - border: 1px solid currentColor; - position: absolute; - right: 5px; - top: 50%; - margin-top: -7px; - line-height: 1; - font-size: 11px; - width: 12px; - height: 12px; - border-radius: 8px; - display: flex; - align-content: center; - justify-content: center; - cursor: help; - &::before { - content: '?'; - } - - } - } - - .rgthree-info-images { - list-style:none; - padding:0; - margin:0; - scroll-snap-type: x mandatory; - display:flex; - flex-direction:row; - overflow: auto; - - > li { - scroll-snap-align: start; - max-width: 90%; - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - overflow: hidden; - padding: 8px; - - .rgthree-info-table { - width: calc(100% - 16px); - } - } - } - - .rgthree-info-civitai-link { - margin: 8px; - color: #eee; - - a, a:hover, a:visited { - color: inherit; - text-decoration: none; - } - - > svg { - width: 16px; - height: 16px; - margin-right: 8px; - } - } -} - - - -.rgthree-lora-chooser-dialog { - max-width: 100%; - - - .rgthree-dialog-container-title { - display: flex; - flex-direction: column; - } - .rgthree-dialog-container-title h2 { - display: flex; - width: 100%; - } - .rgthree-lora-chooser-search { - margin-left: auto; - border-radius: 50px; - width: 50%; - max-width: 170px; - padding: 2px 8px; - } - - .rgthree-lora-chooser-header { - display: flex; - flex-direction: row; - } - - .rgthree-lora-filters-container { - svg {width: 16px; height: 16px;} - } - - .rgthree-dialog-container-content { - width: 80vw; - height: 80vh; - } - - .rgthree-button-reset { - width: 32px; - height: 32px; - > svg {width: 100%; height: 100%;} - - } - - ul.rgthree-lora-chooser-list { - list-style: none; - margin: 0; - padding: 0; - position: relative; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: start; - justify-content: space-around; - - > li { - position: relative; - flex: 0 0 auto; - width: 170px; - max-width: 100%; - margin: 8px 8px 16px; - - label { - position: absolute; - display: block; - inset: 0; - z-index: 3; - cursor: pointer; - } - input[type="checkbox"] { - position: absolute; - right: 8px; - top: 8px; - margin: 0; - z-index: 2; - appearance: none; - background-color: #fff; - width: 48px; - height: 48px; - border-radius: 4px; - border: 1px solid rgba(120,120,120,1); - opacity: 0; - transition: opacity 0.15s ease-in-out; - - &:checked { - opacity: 1; - background: #0060df; - &::before { - content: ""; - display: block; - width: 100%; - height: 100%; - box-shadow: inset 100px 100px #fff; - clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%) - } - } - } - - - figure { - position: relative; - display: block; - margin: 0 0 8px; - padding: 0; - border: 1px solid rgba(120, 120, 120, .8); - background: rgba(120, 120, 120, .5); - width: 100%; - padding-top: 120%; - transition: box-shadow 0.15s ease-in-out; - opacity: 0.75; - &::after { - content: ''; - display: block; - position: absolute; - inset: 0; - } - - &:empty { - &::before { - content: 'No image.'; - color: rgba(200, 200, 200, .8); - position: absolute; - display: block; - inset: 0; - font-size: 1.2em; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - } - } - - > img, > video { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - object-fit: cover; - } - } - div { - word-wrap: break-word; - font-size: 0.8rem; - opacity: 0.75; - } - - &:hover figure::after{ - box-shadow: 0px 2px 6px rgba(0,0,0,0.75); - } - :checked ~ figure::after { - box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88) - } - - &:hover *, - &:hover input[type="checkbox"], - :checked ~ * { - opacity: 1 - } - - } - } -} \ No newline at end of file diff --git a/src_web/common/css/dialog_model_info.scss b/src_web/common/css/dialog_model_info.scss new file mode 100644 index 0000000..da85958 --- /dev/null +++ b/src_web/common/css/dialog_model_info.scss @@ -0,0 +1,475 @@ + +.rgthree-info-dialog { + + width: 90vw; + max-width: 960px; + + .rgthree-info-tags { + list-style: none; + padding: 0; + margin: 0; + display: flex; + + > li { + display: inline-flex; + margin: 0; + vertical-align: top; + + + li { + margin-left: 6px; + } + &:not(.-link) + li.-link { + margin-left: auto; + } + + > * { + border-radius: 4px; + line-height: 1; + color: rgba(255,255,255,0.85); + background: rgb(69, 92, 85);; + font-size: 14px; + font-weight: bold; + text-decoration: none; + display: flex; + height: 1.6em; + padding-left: .5em; + padding-right: .5em; + padding-bottom: .1em; + align-content: center; + justify-content: center; + align-items: center; + box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); + + > svg { + width: 16px; + height: 16px; + + &:first-child { + margin-right: .3em; + display: none; + } + &:last-child { + margin-left: .5em; + fill: currentColor; + } + } + + + &[href] { + box-shadow: inset 0px 1px 0px rgba(255,255,255,0.25), inset 0px -1px 0px rgba(0,0,0,0.66); + } + + &:empty { + display: none; + } + } + + &.-civitai > * { + color: #ddd; + background: #1b65aa; + transition: all 0.15s ease-in-out; + &:hover { + color: #fff; + border-color: #1971c2; + background: #1971c2; + } + } + &.-type > * { + background: rgb(73, 54, 94); + color: rgb(228, 209, 248); + } + } + } + + .rgthree-info-table { + border-collapse: collapse; + margin: 16px 0px; + width: 100%; + font-size: 12px; + + tr.editable button { + display: flex; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; + + svg + svg {display: none;} + } + tr.editable.-rgthree-editing button { + svg {display: none;} + svg + svg {display: inline-block;} + } + + td { + position: relative; + border: 1px solid rgba(255,255,255,0.25); + padding: 0; + vertical-align: top; + + &:first-child { + background: rgba(255,255,255,0.075); + width: 10px; // Small, so it doesn't adjust. + > *:first-child { + white-space: nowrap; + padding-right: 32px; + } + + small { + display: block; + margin-top: 2px; + opacity: 0.75; + + > [data-action] { + text-decoration: underline; + cursor: pointer; + &:hover { + text-decoration: none; + } + } + } + } + + a, a:hover, a:visited { + color: inherit; + } + + svg { + width: 1.3333em; + height: 1.3333em; + vertical-align: -0.285em; + + &.logo-civitai { + margin-right: 0.3333em; + } + } + + > *:first-child { + display: block; + padding: 6px 10px; + } + > input, > textarea{ + padding: 5px 10px; + border: 0; + box-shadow: inset 1px 1px 5px 0px rgba(0,0,0,0.5); + font: inherit; + appearance: none; + background: #fff; + color: #121212; + resize: vertical; + + &:only-child { + width: 100%; + } + } + } + + tr[data-field-name="userNote"] td > span:first-child { + white-space: pre; + } + + tr.rgthree-info-table-break-row td { + border: 0; + background: transparent; + padding: 12px 4px 4px; + font-size: 1.2em; + + > small { + font-style: italic; + opacity: 0.66; + } + + &:empty { + padding: 4px; + } + } + + td .-help { + border: 1px solid currentColor; + position: absolute; + right: 5px; + top: 6px; + line-height: 1; + font-size: 11px; + width: 12px; + height: 12px; + border-radius: 8px; + display: flex; + align-content: center; + justify-content: center; + cursor: help; + &::before { + content: '?'; + } + + } + + td > ul.rgthree-info-trained-words-list { + list-style: none; + padding: 2px 8px; + margin: 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + max-height: 15vh; + overflow: auto; + + > li { + display: inline-flex; + margin: 2px; + vertical-align: top; + border-radius: 4px; + line-height: 1; + color: rgba(255,255,255,0.85); + background: rgb(73, 91, 106); + font-size: 1.2em; + font-weight: 600; + text-decoration: none; + display: flex; + height: 1.6em; + align-content: center; + justify-content: center; + align-items: center; + box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); + cursor: pointer; + white-space: nowrap; + max-width: 183px; + + &:hover { + background: rgb(68, 109, 142); + } + + > svg { + width: auto; + height: 1.2em; + } + + > span { + padding-left: .5em; + padding-right: .5em; + padding-bottom: .1em; + text-overflow: ellipsis; + overflow: hidden; + } + + > small { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.5em; + background: rgba(0,0,0,0.2); + } + + &.-rgthree-is-selected { + background: rgb(42, 126, 193); + } + } + } + } + + .rgthree-info-images { + list-style:none; + padding:0; + margin:0; + scroll-snap-type: x mandatory; + display:flex; + flex-direction:row; + overflow: auto; + + > li { + scroll-snap-align: start; + max-width: 90%; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + overflow: hidden; + padding: 8px; + + .rgthree-info-table { + width: calc(100% - 16px); + } + } + } + + .rgthree-info-civitai-link { + margin: 8px; + color: #eee; + + a, a:hover, a:visited { + color: inherit; + text-decoration: none; + } + + > svg { + width: 16px; + height: 16px; + margin-right: 8px; + } + } +} + + + +.rgthree-lora-chooser-dialog { + max-width: 100%; + + + .rgthree-dialog-container-title { + display: flex; + flex-direction: column; + } + .rgthree-dialog-container-title h2 { + display: flex; + width: 100%; + } + .rgthree-lora-chooser-search { + margin-left: auto; + border-radius: 50px; + width: 50%; + max-width: 170px; + padding: 2px 8px; + } + + .rgthree-lora-chooser-header { + display: flex; + flex-direction: row; + } + + .rgthree-lora-filters-container { + svg {width: 16px; height: 16px;} + } + + .rgthree-dialog-container-content { + width: 80vw; + height: 80vh; + } + + .rgthree-button-reset { + width: 32px; + height: 32px; + > svg {width: 100%; height: 100%;} + + } + + ul.rgthree-lora-chooser-list { + list-style: none; + margin: 0; + padding: 0; + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: start; + justify-content: space-around; + + > li { + position: relative; + flex: 0 0 auto; + width: 170px; + max-width: 100%; + margin: 8px 8px 16px; + + label { + position: absolute; + display: block; + inset: 0; + z-index: 3; + cursor: pointer; + } + input[type="checkbox"] { + position: absolute; + right: 8px; + top: 8px; + margin: 0; + z-index: 2; + appearance: none; + background-color: #fff; + width: 48px; + height: 48px; + border-radius: 4px; + border: 1px solid rgba(120,120,120,1); + opacity: 0; + transition: opacity 0.15s ease-in-out; + + &:checked { + opacity: 1; + background: #0060df; + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + box-shadow: inset 100px 100px #fff; + clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%) + } + } + } + + + figure { + position: relative; + display: block; + margin: 0 0 8px; + padding: 0; + border: 1px solid rgba(120, 120, 120, .8); + background: rgba(120, 120, 120, .5); + width: 100%; + padding-top: 120%; + transition: box-shadow 0.15s ease-in-out; + opacity: 0.75; + &::after { + content: ''; + display: block; + position: absolute; + inset: 0; + } + + &:empty { + &::before { + content: 'No image.'; + color: rgba(200, 200, 200, .8); + position: absolute; + display: block; + inset: 0; + font-size: 1.2em; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + } + } + + > img, > video { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + object-fit: cover; + } + } + div { + word-wrap: break-word; + font-size: 0.8rem; + opacity: 0.75; + } + + &:hover figure::after{ + box-shadow: 0px 2px 6px rgba(0,0,0,0.75); + } + :checked ~ figure::after { + box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88) + } + + &:hover *, + &:hover input[type="checkbox"], + :checked ~ * { + opacity: 1 + } + + } + } +} \ No newline at end of file diff --git a/src_web/typings/rgthree.d.ts b/src_web/typings/rgthree.d.ts index 6ab2bdc..a74bf16 100644 --- a/src_web/typings/rgthree.d.ts +++ b/src_web/typings/rgthree.d.ts @@ -25,10 +25,17 @@ export type RgthreeModelInfo = { name?: string; type?: string; baseModel?: string; + baseModelFile?: string; links?: string[]; strengthMin?: number; strengthMax?: number; triggerWords?: string[]; + trainedWords?: { + word: string; + count?: number; + civitai?: boolean + user?: boolean + }[]; description?: string; sha256?: string; path?: string; diff --git a/web/comfyui/dialog_info.js b/web/comfyui/dialog_info.js index 456bf65..2831d07 100644 --- a/web/comfyui/dialog_info.js +++ b/web/comfyui/dialog_info.js @@ -1,7 +1,25 @@ import { RgthreeDialog } from "../../rgthree/common/dialog.js"; -import { createElement as $el, empty, appendChildren, getClosestOrSelf, queryOne, } from "../../rgthree/common/utils_dom.js"; -import { logoCivitai, link, pencilColored, diskColored, } from "../../rgthree/common/media/svgs.js"; +import { createElement as $el, empty, appendChildren, getClosestOrSelf, queryOne, query, } from "../../rgthree/common/utils_dom.js"; +import { logoCivitai, link, pencilColored, diskColored } from "../../rgthree/common/media/svgs.js"; import { SERVICE as MODEL_INFO_SERVICE } from "../../rgthree/common/model_info_service.js"; +import { rgthree } from "./rgthree.js"; +import { generateId } from "../../rgthree/common/shared_utils.js"; +function injectCss() { + const href = "rgthree/common/css/dialog_model_info.css"; + if (queryOne(`link[href^="${href}"]`)) { + return Promise.resolve(); + } + return new Promise((resolve) => { + const link = $el('link[rel="stylesheet"][type="text/css"]'); + const timeout = setTimeout(resolve, 1000); + link.addEventListener("load", (e) => { + clearInterval(timeout); + resolve(); + }); + link.href = href; + document.head.appendChild(link); + }); +} export class RgthreeInfoDialog extends RgthreeDialog { constructor(file) { const dialogOptions = { @@ -19,7 +37,9 @@ export class RgthreeInfoDialog extends RgthreeDialog { } async init(file) { var _a, _b; + const cssPromise = injectCss(); this.modelInfo = await MODEL_INFO_SERVICE.getLora(file, false, false); + await cssPromise; this.setContent(this.getInfoContent()); this.setTitle(((_a = this.modelInfo) === null || _a === void 0 ? void 0 : _a["name"]) || ((_b = this.modelInfo) === null || _b === void 0 ? void 0 : _b["file"]) || "Unknown"); this.attachEvents(); @@ -32,57 +52,89 @@ export class RgthreeInfoDialog extends RgthreeDialog { } attachEvents() { this.contentElement.addEventListener("click", async (e) => { - var _a, _b; - const info = this.modelInfo; - if (!(info === null || info === void 0 ? void 0 : info.file)) { - return; - } const target = getClosestOrSelf(e.target, "[data-action]"); const action = target === null || target === void 0 ? void 0 : target.getAttribute("data-action"); - if (action === "fetch-civitai") { - this.modelInfo = await MODEL_INFO_SERVICE.refreshLora(info.file); - this.setContent(this.getInfoContent()); - this.setTitle(((_a = this.modelInfo) === null || _a === void 0 ? void 0 : _a["name"]) || ((_b = this.modelInfo) === null || _b === void 0 ? void 0 : _b["file"]) || "Unknown"); - } - else if (action === "edit-row") { - const tr = target.closest("tr"); - const td = queryOne("td:nth-child(2)", tr); - const input = td.querySelector("input,textarea"); - if (!input) { - const fieldName = tr.dataset["fieldName"]; - tr.classList.add("-rgthree-editing"); - const isTextarea = fieldName === "userNote"; - const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, { - value: td.textContent, - }); - input.addEventListener("keydown", (e) => { - if (!isTextarea && e.key === "Enter") { - const modified = saveEditableRow(info, tr, true); - this.modifiedModelData = this.modifiedModelData || modified; - e.stopPropagation(); - e.preventDefault(); - } - else if (e.key === "Escape") { - const modified = saveEditableRow(info, tr, false); - this.modifiedModelData = this.modifiedModelData || modified; - e.stopPropagation(); - e.preventDefault(); - } - }); - appendChildren(empty(td), [input]); - input.focus(); - } - else if (target.nodeName.toLowerCase() === "button") { - const modified = saveEditableRow(info, tr, true); - this.modifiedModelData = this.modifiedModelData || modified; - } - e.preventDefault(); - e.stopPropagation(); + if (!target || !action) { + return; } + await this.handleEventAction(action, target, e); }); } + async handleEventAction(action, target, e) { + var _a, _b; + const info = this.modelInfo; + if (!(info === null || info === void 0 ? void 0 : info.file)) { + return; + } + if (action === "fetch-civitai") { + this.modelInfo = await MODEL_INFO_SERVICE.refreshLora(info.file); + this.setContent(this.getInfoContent()); + this.setTitle(((_a = this.modelInfo) === null || _a === void 0 ? void 0 : _a["name"]) || ((_b = this.modelInfo) === null || _b === void 0 ? void 0 : _b["file"]) || "Unknown"); + } + else if (action === "copy-trained-words") { + const selected = query(".-rgthree-is-selected", target.closest("tr")); + const text = selected.map((el) => el.getAttribute("data-word")).join(", "); + await navigator.clipboard.writeText(text); + rgthree.showMessage({ + id: "copy-trained-words-" + generateId(4), + type: "success", + message: `Successfully copied ${selected.length} key word${selected.length === 1 ? "" : "s"}.`, + timeout: 4000, + }); + } + else if (action === "toggle-trained-word") { + target === null || target === void 0 ? void 0 : target.classList.toggle("-rgthree-is-selected"); + const tr = target.closest("tr"); + if (tr) { + const span = queryOne("td:first-child > *", tr); + let small = queryOne("small", span); + if (!small) { + small = $el("small", { parent: span }); + } + const num = query(".-rgthree-is-selected", tr).length; + small.innerHTML = num + ? `${num} selected | Copy` + : ""; + } + } + else if (action === "edit-row") { + const tr = target.closest("tr"); + const td = queryOne("td:nth-child(2)", tr); + const input = td.querySelector("input,textarea"); + if (!input) { + const fieldName = tr.dataset["fieldName"]; + tr.classList.add("-rgthree-editing"); + const isTextarea = fieldName === "userNote"; + const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, { + value: td.textContent, + }); + input.addEventListener("keydown", (e) => { + if (!isTextarea && e.key === "Enter") { + const modified = saveEditableRow(info, tr, true); + this.modifiedModelData = this.modifiedModelData || modified; + e.stopPropagation(); + e.preventDefault(); + } + else if (e.key === "Escape") { + const modified = saveEditableRow(info, tr, false); + this.modifiedModelData = this.modifiedModelData || modified; + e.stopPropagation(); + e.preventDefault(); + } + }); + appendChildren(empty(td), [input]); + input.focus(); + } + else if (target.nodeName.toLowerCase() === "button") { + const modified = saveEditableRow(info, tr, true); + this.modifiedModelData = this.modifiedModelData || modified; + } + e === null || e === void 0 ? void 0 : e.preventDefault(); + e === null || e === void 0 ? void 0 : e.stopPropagation(); + } + } getInfoContent() { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r; const info = this.modelInfo || {}; const civitaiLink = (_a = info.links) === null || _a === void 0 ? void 0 : _a.find((i) => i.includes("civitai.com/models")); const html = ` @@ -109,18 +161,18 @@ export class RgthreeInfoDialog extends RgthreeDialog { : !((_h = info.raw) === null || _h === void 0 ? void 0 : _h.civitai) ? infoTableRow("Civitai", ``) : ""} - ${infoTableRow("Base Model", info.baseModel || "")} + ${infoTableRow("Base Model", (info.baseModel || " ") + (info.baseModelFile ? `(${info.baseModelFile})` : ""))} + ${infoTableRow("Name", info.name || ((_k = (_j = info.raw) === null || _j === void 0 ? void 0 : _j.metadata) === null || _k === void 0 ? void 0 : _k.ss_output_name) || "", "The name for display.", "name")} - ${infoTableRow("Name", info.name || "", "The name for display.", "name")} - ${infoTableRow("Trigger Words", (_k = (_j = info.triggerWords) === null || _j === void 0 ? void 0 : _j.join(", ")) !== null && _k !== void 0 ? _k : "", "Easily keep track of the trigger words to reference here.", "triggerWords")} - ${infoTableRow("Strength Min", (_l = info.strengthMin) !== null && _l !== void 0 ? _l : "", "The recommended minimum strength, In the Power Lora Loader node, strength will signal when it is below this threshold.", "strengthMin")} - ${infoTableRow("Strength Max", (_m = info.strengthMax) !== null && _m !== void 0 ? _m : "", "The recommended maximum strength. In the Power Lora Loader node, strength will signal when it is above this threshold.", "strengthMax")} + ${infoTableRow("Trained Words", (_l = getTrainedWordsMarkup(info.trainedWords)) !== null && _l !== void 0 ? _l : "", "Trained words from the metadata and/or civitai. Click to select for copy.")} + ${infoTableRow("Strength Min", (_m = info.strengthMin) !== null && _m !== void 0 ? _m : "", "The recommended minimum strength, In the Power Lora Loader node, strength will signal when it is below this threshold.", "strengthMin")} + ${infoTableRow("Strength Max", (_o = info.strengthMax) !== null && _o !== void 0 ? _o : "", "The recommended maximum strength. In the Power Lora Loader node, strength will signal when it is above this threshold.", "strengthMax")} ${""} - ${infoTableRow("Additional Notes", (_o = info.userNote) !== null && _o !== void 0 ? _o : "", "Additional notes you'd like to keep and reference in the info dialog.", "userNote")} + ${infoTableRow("Additional Notes", (_p = info.userNote) !== null && _p !== void 0 ? _p : "", "Additional notes you'd like to keep and reference in the info dialog.", "userNote")} -
    ${(_q = (_p = info.images) === null || _p === void 0 ? void 0 : _p.map((img) => { +
      ${(_r = (_q = info.images) === null || _q === void 0 ? void 0 : _q.map((img) => { var _a; return `
    • @@ -146,7 +198,7 @@ export class RgthreeInfoDialog extends RgthreeDialog { : ""}
    • `; - }).join("")) !== null && _q !== void 0 ? _q : ""}
    + }).join("")) !== null && _r !== void 0 ? _r : ""}
`; const div = $el("div", { html }); return div; @@ -156,12 +208,25 @@ function infoTableRow(name, value, help = "", editableFieldName = "") { return ` ${name} ${help ? `` : ""} - ${value} + ${String(value).startsWith("<") ? value : `${value}`} ${editableFieldName ? `` : ""} `; } +function getTrainedWordsMarkup(words) { + let markup = `
    `; + for (const wordData of words || []) { + markup += `
  • + ${wordData.word} + ${wordData.civitai ? logoCivitai : ""} + ${wordData.count != null ? `${wordData.count}` : ""} +
  • `; + } + markup += `
`; + console.log(markup); + return markup; +} function saveEditableRow(info, tr, saving = true) { var _a; const fieldName = tr.dataset["fieldName"]; diff --git a/web/comfyui/rgthree.css b/web/comfyui/rgthree.css index e778df8..d28059d 100644 --- a/web/comfyui/rgthree.css +++ b/web/comfyui/rgthree.css @@ -336,359 +336,6 @@ body.rgthree-dialog-open > *:not(.rgthree-dialog):not(.rgthree-top-messages-cont filter: blur(5px); } -.rgthree-info-dialog { - min-width: 600px; -} -.rgthree-info-dialog .rgthree-info-tags { - list-style: none; - padding: 0; - margin: 0; - display: flex; -} -.rgthree-info-dialog .rgthree-info-tags > li { - display: inline-flex; - margin: 0; - vertical-align: top; -} -.rgthree-info-dialog .rgthree-info-tags > li + li { - margin-left: 6px; -} -.rgthree-info-dialog .rgthree-info-tags > li:not(.-link) + li.-link { - margin-left: auto; -} -.rgthree-info-dialog .rgthree-info-tags > li > * { - border-radius: 4px; - line-height: 1; - color: rgba(255, 255, 255, 0.85); - background: rgb(69, 92, 85); - font-size: 14px; - font-weight: bold; - text-decoration: none; - display: flex; - height: 1.6em; - padding-left: 0.5em; - padding-right: 0.5em; - padding-bottom: 0.1em; - align-content: center; - justify-content: center; - align-items: center; - box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg { - width: 16px; - height: 16px; -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg:first-child { - margin-right: 0.3em; - display: none; -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg:last-child { - margin-left: 0.5em; - fill: currentColor; -} -.rgthree-info-dialog .rgthree-info-tags > li > *[href] { - box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.25), inset 0px -1px 0px rgba(0, 0, 0, 0.66); -} -.rgthree-info-dialog .rgthree-info-tags > li > *:empty { - display: none; -} -.rgthree-info-dialog .rgthree-info-tags > li.-civitai > * { - color: #ddd; - background: #1b65aa; - transition: all 0.15s ease-in-out; -} -.rgthree-info-dialog .rgthree-info-tags > li.-civitai > *:hover { - color: #fff; - border-color: #1971c2; - background: #1971c2; -} -.rgthree-info-dialog .rgthree-info-tags > li.-type > * { - background: rgb(73, 54, 94); - color: rgb(228, 209, 248); -} -.rgthree-info-dialog .rgthree-info-table { - border-collapse: collapse; - margin: 16px 0px; - width: 100%; - font-size: 12px; -} -.rgthree-info-dialog .rgthree-info-table tr.editable button { - display: flex; - width: 28px; - height: 28px; - align-items: center; - justify-content: center; -} -.rgthree-info-dialog .rgthree-info-table tr.editable button svg + svg { - display: none; -} -.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg { - display: none; -} -.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg + svg { - display: inline-block; -} -.rgthree-info-dialog .rgthree-info-table td { - position: relative; - border: 1px solid rgba(255, 255, 255, 0.25); - padding: 0; -} -.rgthree-info-dialog .rgthree-info-table td:first-child { - background: rgba(255, 255, 255, 0.075); - width: 10px; -} -.rgthree-info-dialog .rgthree-info-table td:first-child > *:first-child { - white-space: nowrap; - padding-right: 32px; -} -.rgthree-info-dialog .rgthree-info-table td a, .rgthree-info-dialog .rgthree-info-table td a:hover, .rgthree-info-dialog .rgthree-info-table td a:visited { - color: inherit; -} -.rgthree-info-dialog .rgthree-info-table td ul { - margin: 0.5em 0; - padding: 0 0 0 1em; -} -.rgthree-info-dialog .rgthree-info-table td svg { - width: 1.3333em; - height: 1.3333em; - vertical-align: -0.285em; -} -.rgthree-info-dialog .rgthree-info-table td svg.logo-civitai { - margin-right: 0.3333em; -} -.rgthree-info-dialog .rgthree-info-table td > *:first-child { - display: block; - padding: 6px 10px; -} -.rgthree-info-dialog .rgthree-info-table td > input, .rgthree-info-dialog .rgthree-info-table td > textarea { - padding: 5px 10px; - border: 0; - box-shadow: inset 1px 1px 5px 0px rgba(0, 0, 0, 0.5); - font: inherit; - appearance: none; - background: #fff; - color: #121212; - resize: vertical; -} -.rgthree-info-dialog .rgthree-info-table td > input:only-child, .rgthree-info-dialog .rgthree-info-table td > textarea:only-child { - width: 100%; -} -.rgthree-info-dialog .rgthree-info-table tr[data-field-name=userNote] td > span:first-child { - white-space: pre; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td { - border: 0; - background: transparent; - padding: 12px 4px 4px; - font-size: 1.2em; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td > small { - font-style: italic; - opacity: 0.66; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td:empty { - padding: 4px; -} -.rgthree-info-dialog .rgthree-info-table td .-help { - border: 1px solid currentColor; - position: absolute; - right: 5px; - top: 50%; - margin-top: -7px; - line-height: 1; - font-size: 11px; - width: 12px; - height: 12px; - border-radius: 8px; - display: flex; - align-content: center; - justify-content: center; - cursor: help; -} -.rgthree-info-dialog .rgthree-info-table td .-help::before { - content: "?"; -} -.rgthree-info-dialog .rgthree-info-images { - list-style: none; - padding: 0; - margin: 0; - scroll-snap-type: x mandatory; - display: flex; - flex-direction: row; - overflow: auto; -} -.rgthree-info-dialog .rgthree-info-images > li { - scroll-snap-align: start; - max-width: 90%; - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - overflow: hidden; - padding: 8px; -} -.rgthree-info-dialog .rgthree-info-images > li .rgthree-info-table { - width: calc(100% - 16px); -} -.rgthree-info-dialog .rgthree-info-civitai-link { - margin: 8px; - color: #eee; -} -.rgthree-info-dialog .rgthree-info-civitai-link a, .rgthree-info-dialog .rgthree-info-civitai-link a:hover, .rgthree-info-dialog .rgthree-info-civitai-link a:visited { - color: inherit; - text-decoration: none; -} -.rgthree-info-dialog .rgthree-info-civitai-link > svg { - width: 16px; - height: 16px; - margin-right: 8px; -} - -.rgthree-lora-chooser-dialog { - max-width: 100%; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-title { - display: flex; - flex-direction: column; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-title h2 { - display: flex; - width: 100%; -} -.rgthree-lora-chooser-dialog .rgthree-lora-chooser-search { - margin-left: auto; - border-radius: 50px; - width: 50%; - max-width: 170px; - padding: 2px 8px; -} -.rgthree-lora-chooser-dialog .rgthree-lora-chooser-header { - display: flex; - flex-direction: row; -} -.rgthree-lora-chooser-dialog .rgthree-lora-filters-container svg { - width: 16px; - height: 16px; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-content { - width: 80vw; - height: 80vh; -} -.rgthree-lora-chooser-dialog .rgthree-button-reset { - width: 32px; - height: 32px; -} -.rgthree-lora-chooser-dialog .rgthree-button-reset > svg { - width: 100%; - height: 100%; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list { - list-style: none; - margin: 0; - padding: 0; - position: relative; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: start; - justify-content: space-around; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li { - position: relative; - flex: 0 0 auto; - width: 170px; - max-width: 100%; - margin: 8px 8px 16px; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li label { - position: absolute; - display: block; - inset: 0; - z-index: 3; - cursor: pointer; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox] { - position: absolute; - right: 8px; - top: 8px; - margin: 0; - z-index: 2; - appearance: none; - background-color: #fff; - width: 48px; - height: 48px; - border-radius: 4px; - border: 1px solid rgb(120, 120, 120); - opacity: 0; - transition: opacity 0.15s ease-in-out; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked { - opacity: 1; - background: #0060df; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked::before { - content: ""; - display: block; - width: 100%; - height: 100%; - box-shadow: inset 100px 100px #fff; - clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure { - position: relative; - display: block; - margin: 0 0 8px; - padding: 0; - border: 1px solid rgba(120, 120, 120, 0.8); - background: rgba(120, 120, 120, 0.5); - width: 100%; - padding-top: 120%; - transition: box-shadow 0.15s ease-in-out; - opacity: 0.75; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure::after { - content: ""; - display: block; - position: absolute; - inset: 0; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure:empty::before { - content: "No image."; - color: rgba(200, 200, 200, 0.8); - position: absolute; - display: block; - inset: 0; - font-size: 1.2em; - text-align: center; - display: flex; - align-items: center; - justify-content: center; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > img, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > video { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - object-fit: cover; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li div { - word-wrap: break-word; - font-size: 0.8rem; - opacity: 0.75; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover figure::after { - box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.75); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ figure::after { - box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover *, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover input[type=checkbox], -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ * { - opacity: 1; -} - .rgthree-dialog.-settings { width: 100%; } diff --git a/web/comfyui/rgthree.js b/web/comfyui/rgthree.js index 569ced5..28e35e4 100644 --- a/web/comfyui/rgthree.js +++ b/web/comfyui/rgthree.js @@ -8,6 +8,7 @@ import { NodeTypesString } from "./constants.js"; import { RgthreeProgressBar } from "../../rgthree/common/progress_bar.js"; import { RgthreeConfigDialog } from "./config.js"; import { iconGear, iconReplace, iconStarFilled, logoRgthree } from "../../rgthree/common/media/svgs.js"; +import { query } from "../../rgthree/common/utils_dom.js"; export var LogLevel; (function (LogLevel) { LogLevel[LogLevel["IMPORTANT"] = 1] = "IMPORTANT"; @@ -539,6 +540,14 @@ class Rgthree extends EventTarget { container.classList.add("rgthree-top-messages-container"); document.body.appendChild(container); } + const dialogs = query('dialog[open]'); + if (dialogs.length) { + let dialog = dialogs[dialogs.length - 1]; + dialog.appendChild(container); + dialog.addEventListener('close', (e) => { + document.body.appendChild(container); + }); + } await this.hideMessage(data.id); const messageContainer = document.createElement("div"); messageContainer.setAttribute("type", data.type || "info"); diff --git a/web/common/css/dialog.css b/web/common/css/dialog.css index 82fb2cb..c9efdf6 100644 --- a/web/common/css/dialog.css +++ b/web/common/css/dialog.css @@ -122,356 +122,3 @@ body.rgthree-dialog-open > *:not(.rgthree-dialog):not(.rgthree-top-messages-container) { filter: blur(5px); } - -.rgthree-info-dialog { - min-width: 600px; -} -.rgthree-info-dialog .rgthree-info-tags { - list-style: none; - padding: 0; - margin: 0; - display: flex; -} -.rgthree-info-dialog .rgthree-info-tags > li { - display: inline-flex; - margin: 0; - vertical-align: top; -} -.rgthree-info-dialog .rgthree-info-tags > li + li { - margin-left: 6px; -} -.rgthree-info-dialog .rgthree-info-tags > li:not(.-link) + li.-link { - margin-left: auto; -} -.rgthree-info-dialog .rgthree-info-tags > li > * { - border-radius: 4px; - line-height: 1; - color: rgba(255, 255, 255, 0.85); - background: rgb(69, 92, 85); - font-size: 14px; - font-weight: bold; - text-decoration: none; - display: flex; - height: 1.6em; - padding-left: 0.5em; - padding-right: 0.5em; - padding-bottom: 0.1em; - align-content: center; - justify-content: center; - align-items: center; - box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg { - width: 16px; - height: 16px; -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg:first-child { - margin-right: 0.3em; - display: none; -} -.rgthree-info-dialog .rgthree-info-tags > li > * > svg:last-child { - margin-left: 0.5em; - fill: currentColor; -} -.rgthree-info-dialog .rgthree-info-tags > li > *[href] { - box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.25), inset 0px -1px 0px rgba(0, 0, 0, 0.66); -} -.rgthree-info-dialog .rgthree-info-tags > li > *:empty { - display: none; -} -.rgthree-info-dialog .rgthree-info-tags > li.-civitai > * { - color: #ddd; - background: #1b65aa; - transition: all 0.15s ease-in-out; -} -.rgthree-info-dialog .rgthree-info-tags > li.-civitai > *:hover { - color: #fff; - border-color: #1971c2; - background: #1971c2; -} -.rgthree-info-dialog .rgthree-info-tags > li.-type > * { - background: rgb(73, 54, 94); - color: rgb(228, 209, 248); -} -.rgthree-info-dialog .rgthree-info-table { - border-collapse: collapse; - margin: 16px 0px; - width: 100%; - font-size: 12px; -} -.rgthree-info-dialog .rgthree-info-table tr.editable button { - display: flex; - width: 28px; - height: 28px; - align-items: center; - justify-content: center; -} -.rgthree-info-dialog .rgthree-info-table tr.editable button svg + svg { - display: none; -} -.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg { - display: none; -} -.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg + svg { - display: inline-block; -} -.rgthree-info-dialog .rgthree-info-table td { - position: relative; - border: 1px solid rgba(255, 255, 255, 0.25); - padding: 0; -} -.rgthree-info-dialog .rgthree-info-table td:first-child { - background: rgba(255, 255, 255, 0.075); - width: 10px; -} -.rgthree-info-dialog .rgthree-info-table td:first-child > *:first-child { - white-space: nowrap; - padding-right: 32px; -} -.rgthree-info-dialog .rgthree-info-table td a, .rgthree-info-dialog .rgthree-info-table td a:hover, .rgthree-info-dialog .rgthree-info-table td a:visited { - color: inherit; -} -.rgthree-info-dialog .rgthree-info-table td ul { - margin: 0.5em 0; - padding: 0 0 0 1em; -} -.rgthree-info-dialog .rgthree-info-table td svg { - width: 1.3333em; - height: 1.3333em; - vertical-align: -0.285em; -} -.rgthree-info-dialog .rgthree-info-table td svg.logo-civitai { - margin-right: 0.3333em; -} -.rgthree-info-dialog .rgthree-info-table td > *:first-child { - display: block; - padding: 6px 10px; -} -.rgthree-info-dialog .rgthree-info-table td > input, .rgthree-info-dialog .rgthree-info-table td > textarea { - padding: 5px 10px; - border: 0; - box-shadow: inset 1px 1px 5px 0px rgba(0, 0, 0, 0.5); - font: inherit; - appearance: none; - background: #fff; - color: #121212; - resize: vertical; -} -.rgthree-info-dialog .rgthree-info-table td > input:only-child, .rgthree-info-dialog .rgthree-info-table td > textarea:only-child { - width: 100%; -} -.rgthree-info-dialog .rgthree-info-table tr[data-field-name=userNote] td > span:first-child { - white-space: pre; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td { - border: 0; - background: transparent; - padding: 12px 4px 4px; - font-size: 1.2em; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td > small { - font-style: italic; - opacity: 0.66; -} -.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td:empty { - padding: 4px; -} -.rgthree-info-dialog .rgthree-info-table td .-help { - border: 1px solid currentColor; - position: absolute; - right: 5px; - top: 50%; - margin-top: -7px; - line-height: 1; - font-size: 11px; - width: 12px; - height: 12px; - border-radius: 8px; - display: flex; - align-content: center; - justify-content: center; - cursor: help; -} -.rgthree-info-dialog .rgthree-info-table td .-help::before { - content: "?"; -} -.rgthree-info-dialog .rgthree-info-images { - list-style: none; - padding: 0; - margin: 0; - scroll-snap-type: x mandatory; - display: flex; - flex-direction: row; - overflow: auto; -} -.rgthree-info-dialog .rgthree-info-images > li { - scroll-snap-align: start; - max-width: 90%; - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - overflow: hidden; - padding: 8px; -} -.rgthree-info-dialog .rgthree-info-images > li .rgthree-info-table { - width: calc(100% - 16px); -} -.rgthree-info-dialog .rgthree-info-civitai-link { - margin: 8px; - color: #eee; -} -.rgthree-info-dialog .rgthree-info-civitai-link a, .rgthree-info-dialog .rgthree-info-civitai-link a:hover, .rgthree-info-dialog .rgthree-info-civitai-link a:visited { - color: inherit; - text-decoration: none; -} -.rgthree-info-dialog .rgthree-info-civitai-link > svg { - width: 16px; - height: 16px; - margin-right: 8px; -} - -.rgthree-lora-chooser-dialog { - max-width: 100%; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-title { - display: flex; - flex-direction: column; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-title h2 { - display: flex; - width: 100%; -} -.rgthree-lora-chooser-dialog .rgthree-lora-chooser-search { - margin-left: auto; - border-radius: 50px; - width: 50%; - max-width: 170px; - padding: 2px 8px; -} -.rgthree-lora-chooser-dialog .rgthree-lora-chooser-header { - display: flex; - flex-direction: row; -} -.rgthree-lora-chooser-dialog .rgthree-lora-filters-container svg { - width: 16px; - height: 16px; -} -.rgthree-lora-chooser-dialog .rgthree-dialog-container-content { - width: 80vw; - height: 80vh; -} -.rgthree-lora-chooser-dialog .rgthree-button-reset { - width: 32px; - height: 32px; -} -.rgthree-lora-chooser-dialog .rgthree-button-reset > svg { - width: 100%; - height: 100%; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list { - list-style: none; - margin: 0; - padding: 0; - position: relative; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: start; - justify-content: space-around; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li { - position: relative; - flex: 0 0 auto; - width: 170px; - max-width: 100%; - margin: 8px 8px 16px; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li label { - position: absolute; - display: block; - inset: 0; - z-index: 3; - cursor: pointer; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox] { - position: absolute; - right: 8px; - top: 8px; - margin: 0; - z-index: 2; - appearance: none; - background-color: #fff; - width: 48px; - height: 48px; - border-radius: 4px; - border: 1px solid rgb(120, 120, 120); - opacity: 0; - transition: opacity 0.15s ease-in-out; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked { - opacity: 1; - background: #0060df; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked::before { - content: ""; - display: block; - width: 100%; - height: 100%; - box-shadow: inset 100px 100px #fff; - clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure { - position: relative; - display: block; - margin: 0 0 8px; - padding: 0; - border: 1px solid rgba(120, 120, 120, 0.8); - background: rgba(120, 120, 120, 0.5); - width: 100%; - padding-top: 120%; - transition: box-shadow 0.15s ease-in-out; - opacity: 0.75; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure::after { - content: ""; - display: block; - position: absolute; - inset: 0; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure:empty::before { - content: "No image."; - color: rgba(200, 200, 200, 0.8); - position: absolute; - display: block; - inset: 0; - font-size: 1.2em; - text-align: center; - display: flex; - align-items: center; - justify-content: center; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > img, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > video { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - object-fit: cover; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li div { - word-wrap: break-word; - font-size: 0.8rem; - opacity: 0.75; -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover figure::after { - box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.75); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ figure::after { - box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88); -} -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover *, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover input[type=checkbox], -.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ * { - opacity: 1; -} diff --git a/web/common/css/dialog_model_info.css b/web/common/css/dialog_model_info.css new file mode 100644 index 0000000..6dbd6be --- /dev/null +++ b/web/common/css/dialog_model_info.css @@ -0,0 +1,417 @@ +.rgthree-info-dialog { + width: 90vw; + max-width: 960px; +} +.rgthree-info-dialog .rgthree-info-tags { + list-style: none; + padding: 0; + margin: 0; + display: flex; +} +.rgthree-info-dialog .rgthree-info-tags > li { + display: inline-flex; + margin: 0; + vertical-align: top; +} +.rgthree-info-dialog .rgthree-info-tags > li + li { + margin-left: 6px; +} +.rgthree-info-dialog .rgthree-info-tags > li:not(.-link) + li.-link { + margin-left: auto; +} +.rgthree-info-dialog .rgthree-info-tags > li > * { + border-radius: 4px; + line-height: 1; + color: rgba(255, 255, 255, 0.85); + background: rgb(69, 92, 85); + font-size: 14px; + font-weight: bold; + text-decoration: none; + display: flex; + height: 1.6em; + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.1em; + align-content: center; + justify-content: center; + align-items: center; + box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); +} +.rgthree-info-dialog .rgthree-info-tags > li > * > svg { + width: 16px; + height: 16px; +} +.rgthree-info-dialog .rgthree-info-tags > li > * > svg:first-child { + margin-right: 0.3em; + display: none; +} +.rgthree-info-dialog .rgthree-info-tags > li > * > svg:last-child { + margin-left: 0.5em; + fill: currentColor; +} +.rgthree-info-dialog .rgthree-info-tags > li > *[href] { + box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.25), inset 0px -1px 0px rgba(0, 0, 0, 0.66); +} +.rgthree-info-dialog .rgthree-info-tags > li > *:empty { + display: none; +} +.rgthree-info-dialog .rgthree-info-tags > li.-civitai > * { + color: #ddd; + background: #1b65aa; + transition: all 0.15s ease-in-out; +} +.rgthree-info-dialog .rgthree-info-tags > li.-civitai > *:hover { + color: #fff; + border-color: #1971c2; + background: #1971c2; +} +.rgthree-info-dialog .rgthree-info-tags > li.-type > * { + background: rgb(73, 54, 94); + color: rgb(228, 209, 248); +} +.rgthree-info-dialog .rgthree-info-table { + border-collapse: collapse; + margin: 16px 0px; + width: 100%; + font-size: 12px; +} +.rgthree-info-dialog .rgthree-info-table tr.editable button { + display: flex; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; +} +.rgthree-info-dialog .rgthree-info-table tr.editable button svg + svg { + display: none; +} +.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg { + display: none; +} +.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg + svg { + display: inline-block; +} +.rgthree-info-dialog .rgthree-info-table td { + position: relative; + border: 1px solid rgba(255, 255, 255, 0.25); + padding: 0; + vertical-align: top; +} +.rgthree-info-dialog .rgthree-info-table td:first-child { + background: rgba(255, 255, 255, 0.075); + width: 10px; +} +.rgthree-info-dialog .rgthree-info-table td:first-child > *:first-child { + white-space: nowrap; + padding-right: 32px; +} +.rgthree-info-dialog .rgthree-info-table td:first-child small { + display: block; + margin-top: 2px; + opacity: 0.75; +} +.rgthree-info-dialog .rgthree-info-table td:first-child small > [data-action] { + text-decoration: underline; + cursor: pointer; +} +.rgthree-info-dialog .rgthree-info-table td:first-child small > [data-action]:hover { + text-decoration: none; +} +.rgthree-info-dialog .rgthree-info-table td a, .rgthree-info-dialog .rgthree-info-table td a:hover, .rgthree-info-dialog .rgthree-info-table td a:visited { + color: inherit; +} +.rgthree-info-dialog .rgthree-info-table td svg { + width: 1.3333em; + height: 1.3333em; + vertical-align: -0.285em; +} +.rgthree-info-dialog .rgthree-info-table td svg.logo-civitai { + margin-right: 0.3333em; +} +.rgthree-info-dialog .rgthree-info-table td > *:first-child { + display: block; + padding: 6px 10px; +} +.rgthree-info-dialog .rgthree-info-table td > input, .rgthree-info-dialog .rgthree-info-table td > textarea { + padding: 5px 10px; + border: 0; + box-shadow: inset 1px 1px 5px 0px rgba(0, 0, 0, 0.5); + font: inherit; + appearance: none; + background: #fff; + color: #121212; + resize: vertical; +} +.rgthree-info-dialog .rgthree-info-table td > input:only-child, .rgthree-info-dialog .rgthree-info-table td > textarea:only-child { + width: 100%; +} +.rgthree-info-dialog .rgthree-info-table tr[data-field-name=userNote] td > span:first-child { + white-space: pre; +} +.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td { + border: 0; + background: transparent; + padding: 12px 4px 4px; + font-size: 1.2em; +} +.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td > small { + font-style: italic; + opacity: 0.66; +} +.rgthree-info-dialog .rgthree-info-table tr.rgthree-info-table-break-row td:empty { + padding: 4px; +} +.rgthree-info-dialog .rgthree-info-table td .-help { + border: 1px solid currentColor; + position: absolute; + right: 5px; + top: 6px; + line-height: 1; + font-size: 11px; + width: 12px; + height: 12px; + border-radius: 8px; + display: flex; + align-content: center; + justify-content: center; + cursor: help; +} +.rgthree-info-dialog .rgthree-info-table td .-help::before { + content: "?"; +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list { + list-style: none; + padding: 2px 8px; + margin: 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + max-height: 15vh; + overflow: auto; +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li { + display: inline-flex; + margin: 2px; + vertical-align: top; + border-radius: 4px; + line-height: 1; + color: rgba(255, 255, 255, 0.85); + background: rgb(73, 91, 106); + font-size: 1.2em; + font-weight: 600; + text-decoration: none; + display: flex; + height: 1.6em; + align-content: center; + justify-content: center; + align-items: center; + box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5); + cursor: pointer; + white-space: nowrap; + max-width: 183px; +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li:hover { + background: rgb(68, 109, 142); +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > svg { + width: auto; + height: 1.2em; +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > span { + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.1em; + text-overflow: ellipsis; + overflow: hidden; +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > small { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.5em; + background: rgba(0, 0, 0, 0.2); +} +.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li.-rgthree-is-selected { + background: rgb(42, 126, 193); +} +.rgthree-info-dialog .rgthree-info-images { + list-style: none; + padding: 0; + margin: 0; + scroll-snap-type: x mandatory; + display: flex; + flex-direction: row; + overflow: auto; +} +.rgthree-info-dialog .rgthree-info-images > li { + scroll-snap-align: start; + max-width: 90%; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + overflow: hidden; + padding: 8px; +} +.rgthree-info-dialog .rgthree-info-images > li .rgthree-info-table { + width: calc(100% - 16px); +} +.rgthree-info-dialog .rgthree-info-civitai-link { + margin: 8px; + color: #eee; +} +.rgthree-info-dialog .rgthree-info-civitai-link a, .rgthree-info-dialog .rgthree-info-civitai-link a:hover, .rgthree-info-dialog .rgthree-info-civitai-link a:visited { + color: inherit; + text-decoration: none; +} +.rgthree-info-dialog .rgthree-info-civitai-link > svg { + width: 16px; + height: 16px; + margin-right: 8px; +} + +.rgthree-lora-chooser-dialog { + max-width: 100%; +} +.rgthree-lora-chooser-dialog .rgthree-dialog-container-title { + display: flex; + flex-direction: column; +} +.rgthree-lora-chooser-dialog .rgthree-dialog-container-title h2 { + display: flex; + width: 100%; +} +.rgthree-lora-chooser-dialog .rgthree-lora-chooser-search { + margin-left: auto; + border-radius: 50px; + width: 50%; + max-width: 170px; + padding: 2px 8px; +} +.rgthree-lora-chooser-dialog .rgthree-lora-chooser-header { + display: flex; + flex-direction: row; +} +.rgthree-lora-chooser-dialog .rgthree-lora-filters-container svg { + width: 16px; + height: 16px; +} +.rgthree-lora-chooser-dialog .rgthree-dialog-container-content { + width: 80vw; + height: 80vh; +} +.rgthree-lora-chooser-dialog .rgthree-button-reset { + width: 32px; + height: 32px; +} +.rgthree-lora-chooser-dialog .rgthree-button-reset > svg { + width: 100%; + height: 100%; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list { + list-style: none; + margin: 0; + padding: 0; + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: start; + justify-content: space-around; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li { + position: relative; + flex: 0 0 auto; + width: 170px; + max-width: 100%; + margin: 8px 8px 16px; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li label { + position: absolute; + display: block; + inset: 0; + z-index: 3; + cursor: pointer; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox] { + position: absolute; + right: 8px; + top: 8px; + margin: 0; + z-index: 2; + appearance: none; + background-color: #fff; + width: 48px; + height: 48px; + border-radius: 4px; + border: 1px solid rgb(120, 120, 120); + opacity: 0; + transition: opacity 0.15s ease-in-out; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked { + opacity: 1; + background: #0060df; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li input[type=checkbox]:checked::before { + content: ""; + display: block; + width: 100%; + height: 100%; + box-shadow: inset 100px 100px #fff; + clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%); +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure { + position: relative; + display: block; + margin: 0 0 8px; + padding: 0; + border: 1px solid rgba(120, 120, 120, 0.8); + background: rgba(120, 120, 120, 0.5); + width: 100%; + padding-top: 120%; + transition: box-shadow 0.15s ease-in-out; + opacity: 0.75; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure::after { + content: ""; + display: block; + position: absolute; + inset: 0; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure:empty::before { + content: "No image."; + color: rgba(200, 200, 200, 0.8); + position: absolute; + display: block; + inset: 0; + font-size: 1.2em; + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > img, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li figure > video { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + object-fit: cover; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li div { + word-wrap: break-word; + font-size: 0.8rem; + opacity: 0.75; +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover figure::after { + box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.75); +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ figure::after { + box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88); +} +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover *, .rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li:hover input[type=checkbox], +.rgthree-lora-chooser-dialog ul.rgthree-lora-chooser-list > li :checked ~ * { + opacity: 1; +}