Added the ability to create and edit tags for LoRA
This commit is contained in:
@@ -326,5 +326,105 @@ async def rayko_lora_loader_fetch_civitai_info(request):
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
return aiohttp.web.json_response({"error": "network", "message": f"Network error: {str(e)}"})
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko_lora_loader/add_tags")
|
||||
async def rayko_add_tags(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
lora_name = data.get("name", "")
|
||||
new_tags = data.get("tags", [])
|
||||
|
||||
if not lora_name:
|
||||
return aiohttp.web.Response(status=400, text="No name provided")
|
||||
if not isinstance(new_tags, list) or not new_tags:
|
||||
return aiohttp.web.json_response({"error": "No tags provided"})
|
||||
|
||||
lora_relative_path = lora_name.replace("\\", "/")
|
||||
lora_full_path = folder_paths.get_full_path("loras", lora_relative_path)
|
||||
|
||||
if not lora_full_path or not os.path.exists(lora_full_path):
|
||||
return aiohttp.web.json_response({"error": "File not found"})
|
||||
|
||||
file_hash = compute_sha256(lora_full_path)
|
||||
db_path = os.path.join(RAYKO_LORA_DATA_DIR, f"{file_hash}.json")
|
||||
|
||||
existing_data = {}
|
||||
if os.path.exists(db_path):
|
||||
try:
|
||||
with open(db_path, 'r', encoding='utf-8') as f:
|
||||
existing_data = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
existing_tags = existing_data.get("trained_words", [])
|
||||
if not isinstance(existing_tags, list):
|
||||
existing_tags = []
|
||||
|
||||
combined_tags = existing_tags.copy()
|
||||
for tag in new_tags:
|
||||
tag = str(tag).strip()
|
||||
if tag and tag not in combined_tags:
|
||||
combined_tags.append(tag)
|
||||
|
||||
save_data = {
|
||||
"full_name": existing_data.get("name", ""),
|
||||
"trained_words": combined_tags,
|
||||
"description": existing_data.get("description", "")
|
||||
}
|
||||
|
||||
save_to_rayko_db(file_hash, save_data, "manual", overwrite=True)
|
||||
|
||||
return aiohttp.web.json_response({
|
||||
"trained_words": combined_tags,
|
||||
"added": len(combined_tags) - len(existing_tags)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko_lora_loader/remove_tag")
|
||||
async def rayko_remove_tag(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
lora_name = data.get("name", "")
|
||||
tag_to_remove = data.get("tag", "")
|
||||
|
||||
if not lora_name or not tag_to_remove:
|
||||
return aiohttp.web.Response(status=400, text="Missing parameters")
|
||||
|
||||
lora_relative_path = lora_name.replace("\\", "/")
|
||||
lora_full_path = folder_paths.get_full_path("loras", lora_relative_path)
|
||||
|
||||
if not lora_full_path or not os.path.exists(lora_full_path):
|
||||
return aiohttp.web.json_response({"error": "File not found"})
|
||||
|
||||
file_hash = compute_sha256(lora_full_path)
|
||||
db_path = os.path.join(RAYKO_LORA_DATA_DIR, f"{file_hash}.json")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
return aiohttp.web.json_response({"error": "No data found"})
|
||||
|
||||
with open(db_path, 'r', encoding='utf-8') as f:
|
||||
existing_data = json.load(f)
|
||||
|
||||
existing_tags = existing_data.get("trained_words", [])
|
||||
if tag_to_remove in existing_tags:
|
||||
existing_tags.remove(tag_to_remove)
|
||||
|
||||
save_data = {
|
||||
"full_name": existing_data.get("name", ""),
|
||||
"trained_words": existing_tags,
|
||||
"description": existing_data.get("description", "")
|
||||
}
|
||||
|
||||
save_to_rayko_db(file_hash, save_data, existing_data.get("source", "manual"), overwrite=True)
|
||||
|
||||
return aiohttp.web.json_response({
|
||||
"trained_words": existing_tags,
|
||||
"removed": tag_to_remove
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
@@ -396,5 +396,105 @@ async def rayko_loras_delete_preset(request):
|
||||
os.remove(filepath)
|
||||
return aiohttp.web.Response(status=200, text="OK")
|
||||
return aiohttp.web.Response(status=404, text="Preset not found")
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko/add_tags")
|
||||
async def rayko_add_tags(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
lora_name = data.get("name", "")
|
||||
new_tags = data.get("tags", [])
|
||||
|
||||
if not lora_name:
|
||||
return aiohttp.web.Response(status=400, text="No name provided")
|
||||
if not isinstance(new_tags, list) or not new_tags:
|
||||
return aiohttp.web.json_response({"error": "No tags provided"})
|
||||
|
||||
lora_relative_path = lora_name.replace("\\", "/")
|
||||
lora_full_path = folder_paths.get_full_path("loras", lora_relative_path)
|
||||
|
||||
if not lora_full_path or not os.path.exists(lora_full_path):
|
||||
return aiohttp.web.json_response({"error": "File not found"})
|
||||
|
||||
file_hash = compute_sha256(lora_full_path)
|
||||
db_path = os.path.join(RAYKO_LORA_DATA_DIR, f"{file_hash}.json")
|
||||
|
||||
existing_data = {}
|
||||
if os.path.exists(db_path):
|
||||
try:
|
||||
with open(db_path, 'r', encoding='utf-8') as f:
|
||||
existing_data = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
existing_tags = existing_data.get("trained_words", [])
|
||||
if not isinstance(existing_tags, list):
|
||||
existing_tags = []
|
||||
|
||||
combined_tags = existing_tags.copy()
|
||||
for tag in new_tags:
|
||||
tag = str(tag).strip()
|
||||
if tag and tag not in combined_tags:
|
||||
combined_tags.append(tag)
|
||||
|
||||
save_data = {
|
||||
"full_name": existing_data.get("name", ""),
|
||||
"trained_words": combined_tags,
|
||||
"description": existing_data.get("description", "")
|
||||
}
|
||||
|
||||
save_to_rayko_db(file_hash, save_data, "manual", overwrite=True)
|
||||
|
||||
return aiohttp.web.json_response({
|
||||
"trained_words": combined_tags,
|
||||
"added": len(combined_tags) - len(existing_tags)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko/remove_tag")
|
||||
async def rayko_remove_tag(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
lora_name = data.get("name", "")
|
||||
tag_to_remove = data.get("tag", "")
|
||||
|
||||
if not lora_name or not tag_to_remove:
|
||||
return aiohttp.web.Response(status=400, text="Missing parameters")
|
||||
|
||||
lora_relative_path = lora_name.replace("\\", "/")
|
||||
lora_full_path = folder_paths.get_full_path("loras", lora_relative_path)
|
||||
|
||||
if not lora_full_path or not os.path.exists(lora_full_path):
|
||||
return aiohttp.web.json_response({"error": "File not found"})
|
||||
|
||||
file_hash = compute_sha256(lora_full_path)
|
||||
db_path = os.path.join(RAYKO_LORA_DATA_DIR, f"{file_hash}.json")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
return aiohttp.web.json_response({"error": "No data found"})
|
||||
|
||||
with open(db_path, 'r', encoding='utf-8') as f:
|
||||
existing_data = json.load(f)
|
||||
|
||||
existing_tags = existing_data.get("trained_words", [])
|
||||
if tag_to_remove in existing_tags:
|
||||
existing_tags.remove(tag_to_remove)
|
||||
|
||||
save_data = {
|
||||
"full_name": existing_data.get("name", ""),
|
||||
"trained_words": existing_tags,
|
||||
"description": existing_data.get("description", "")
|
||||
}
|
||||
|
||||
save_to_rayko_db(file_hash, save_data, existing_data.get("source", "manual"), overwrite=True)
|
||||
|
||||
return aiohttp.web.json_response({
|
||||
"trained_words": existing_tags,
|
||||
"removed": tag_to_remove
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return aiohttp.web.Response(status=500, text=str(e))
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.31.1"
|
||||
__version__ = "0.31.2"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.31.1",
|
||||
"version": "0.31.2",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.31.1"
|
||||
version = "0.31.2"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
+187
-17
@@ -1231,7 +1231,54 @@ app.registerExtension({
|
||||
const handleEsc = (e) => { if (e.key === "Escape") { popup.remove(); document.removeEventListener("keydown", handleEsc); } };
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
|
||||
const updateContent = (data) => {
|
||||
let currentData = null;
|
||||
|
||||
const showDeleteConfirmation = (tag, callback) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "rayko-delete-confirm-overlay";
|
||||
overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100002; display: flex; justify-content: center; align-items: center;`;
|
||||
|
||||
const confirmBox = document.createElement("div");
|
||||
confirmBox.style.cssText = `background: #1a1a1a; border: 1px solid #f44336; border-radius: 8px; padding: 20px; max-width: 350px; box-shadow: 0 4px 20px rgba(0,0,0,0.9);`;
|
||||
|
||||
const message = document.createElement("div");
|
||||
message.textContent = `Remove tag "${tag}"?`;
|
||||
message.style.cssText = `color: #fff; font-size: 14px; margin-bottom: 20px; text-align: center;`;
|
||||
|
||||
const btnsDiv = document.createElement("div");
|
||||
btnsDiv.style.cssText = `display: flex; gap: 10px;`;
|
||||
|
||||
const yesBtn = document.createElement("button");
|
||||
yesBtn.textContent = "Yes";
|
||||
yesBtn.style.cssText = `flex: 1; background: #3a1a1a; color: #f44336; border: 1px solid #f44336; border-radius: 4px; padding: 8px; cursor: pointer; font-size: 12px;`;
|
||||
yesBtn.onmouseenter = () => yesBtn.style.background = "#4a2a2a";
|
||||
yesBtn.onmouseleave = () => yesBtn.style.background = "#3a1a1a";
|
||||
yesBtn.onclick = () => {
|
||||
overlay.remove();
|
||||
callback(true);
|
||||
};
|
||||
|
||||
const noBtn = document.createElement("button");
|
||||
noBtn.textContent = "No";
|
||||
noBtn.style.cssText = `flex: 1; background: #2a2a2a; color: #ccc; border: 1px solid #555; border-radius: 4px; padding: 8px; cursor: pointer; font-size: 12px;`;
|
||||
noBtn.onmouseenter = () => noBtn.style.background = "#3a3a3a";
|
||||
noBtn.onmouseleave = () => noBtn.style.background = "#2a2a2a";
|
||||
noBtn.onclick = () => {
|
||||
overlay.remove();
|
||||
callback(false);
|
||||
};
|
||||
|
||||
btnsDiv.appendChild(yesBtn);
|
||||
btnsDiv.appendChild(noBtn);
|
||||
confirmBox.appendChild(message);
|
||||
confirmBox.appendChild(btnsDiv);
|
||||
overlay.appendChild(confirmBox);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
yesBtn.focus();
|
||||
};
|
||||
|
||||
const renderChips = (data) => {
|
||||
content.innerHTML = "";
|
||||
sourceLabel.textContent = `Source: ${data.source || 'unknown'}`;
|
||||
|
||||
@@ -1256,16 +1303,53 @@ app.registerExtension({
|
||||
|
||||
data.trained_words.forEach(word => {
|
||||
const chip = document.createElement("div");
|
||||
chip.textContent = word;
|
||||
chip.style.cssText = `background: #2a3a2a; color: #4CAF50; padding: 4px 10px; border-radius: 12px; font-size: 11px; border: 1px solid #4CAF50; cursor: pointer; user-select: none;`;
|
||||
chip.style.cssText = `background: #2a3a2a; color: #4CAF50; padding: 4px 10px; border-radius: 12px; font-size: 11px; border: 1px solid #4CAF50; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; position: relative;`;
|
||||
|
||||
const textSpan = document.createElement("span");
|
||||
textSpan.textContent = word;
|
||||
textSpan.style.cssText = `flex: 1;`;
|
||||
chip.appendChild(textSpan);
|
||||
|
||||
const deleteBtn = document.createElement("span");
|
||||
deleteBtn.textContent = "✕";
|
||||
deleteBtn.style.cssText = `font-size: 10px; opacity: 0.5; cursor: pointer; padding: 0 2px; line-height: 1;`;
|
||||
deleteBtn.onmouseenter = () => { deleteBtn.style.opacity = "1"; deleteBtn.style.color = "#f44336"; };
|
||||
deleteBtn.onmouseleave = () => { deleteBtn.style.opacity = "0.5"; deleteBtn.style.color = "#4CAF50"; };
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
showDeleteConfirmation(word, (confirmed) => {
|
||||
if (confirmed) {
|
||||
fetch("/rayko/remove_tag", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName, tag: word })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
showRaykoToast(result.error, "error", this);
|
||||
} else {
|
||||
showRaykoToast(`Tag "${word}" removed`, "success", this);
|
||||
currentData = { ...currentData, trained_words: result.trained_words };
|
||||
renderChips(currentData);
|
||||
}
|
||||
})
|
||||
.catch(err => showRaykoToast("Failed to remove tag", "error", this));
|
||||
}
|
||||
});
|
||||
};
|
||||
chip.appendChild(deleteBtn);
|
||||
|
||||
chip.onmouseenter = () => { chip.style.background = "#3a5a3a"; chip.style.borderColor = "#66dd66"; };
|
||||
chip.onmouseleave = () => { chip.style.background = "#2a3a2a"; chip.style.borderColor = "#4CAF50"; };
|
||||
chip.onclick = () => {
|
||||
navigator.clipboard.writeText(word).then(() => {
|
||||
showRaykoToast(`Copied: "${word}"`, "success", this);
|
||||
popup.remove();
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
chip.onclick = (e) => {
|
||||
if (e.target !== deleteBtn && !deleteBtn.contains(e.target)) {
|
||||
navigator.clipboard.writeText(word).then(() => {
|
||||
showRaykoToast(`Copied: "${word}"`, "success", this);
|
||||
popup.remove();
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
}
|
||||
};
|
||||
twContainer.appendChild(chip);
|
||||
});
|
||||
@@ -1285,6 +1369,9 @@ app.registerExtension({
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
};
|
||||
content.appendChild(copyBtn);
|
||||
} else {
|
||||
content.textContent = "No trained words found";
|
||||
content.style.color = "#888";
|
||||
}
|
||||
|
||||
if (data.description) {
|
||||
@@ -1299,16 +1386,22 @@ app.registerExtension({
|
||||
content.appendChild(desc);
|
||||
}
|
||||
|
||||
if (!data.trained_words?.length && !data.description) {
|
||||
content.textContent = "No metadata available";
|
||||
content.style.color = "#888";
|
||||
}
|
||||
|
||||
const actionsDiv = document.createElement("div");
|
||||
actionsDiv.style.cssText = `display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap;`;
|
||||
|
||||
const addTagsBtn = document.createElement("button");
|
||||
const hasTags = data.trained_words && data.trained_words.length > 0;
|
||||
addTagsBtn.textContent = "➕ Add Tags";
|
||||
addTagsBtn.style.cssText = hasTags
|
||||
? `flex: 1; background: #2a2a2a; color: #aaa; border: 1px solid #555; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`
|
||||
: `flex: 1; background: #3a2a1a; color: #fbbf24; border: 1px solid #fbbf24; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
addTagsBtn.onmouseenter = () => addTagsBtn.style.background = hasTags ? "#3a3a3a" : "#4a3a2a";
|
||||
addTagsBtn.onmouseleave = () => addTagsBtn.style.background = hasTags ? "#2a2a2a" : "#3a2a1a";
|
||||
addTagsBtn.onclick = () => showTextarea();
|
||||
actionsDiv.appendChild(addTagsBtn);
|
||||
|
||||
const fetchBtn = document.createElement("button");
|
||||
fetchBtn.textContent = "🌐 Fetch from Civitai";
|
||||
fetchBtn.textContent = " Fetch from Civitai";
|
||||
fetchBtn.style.cssText = `flex: 1; background: #3a2a1a; color: #fbbf24; border: 1px solid #fbbf24; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
fetchBtn.onmouseenter = () => fetchBtn.style.background = "#4a3a2a";
|
||||
fetchBtn.onmouseleave = () => fetchBtn.style.background = "#3a2a1a";
|
||||
@@ -1329,7 +1422,8 @@ app.registerExtension({
|
||||
content.style.color = "#f44336";
|
||||
sourceLabel.textContent = "";
|
||||
} else {
|
||||
updateContent(result);
|
||||
currentData = result;
|
||||
renderChips(result);
|
||||
}
|
||||
} catch (err) {
|
||||
content.textContent = "Network error";
|
||||
@@ -1342,13 +1436,89 @@ app.registerExtension({
|
||||
content.appendChild(actionsDiv);
|
||||
};
|
||||
|
||||
const showTextarea = () => {
|
||||
content.innerHTML = "";
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.style.cssText = `width: 100%; height: 120px; background: #111; color: #fff; border: 1px solid #444; border-radius: 4px; padding: 8px; font-size: 12px; font-family: monospace; resize: vertical; box-sizing: border-box;`;
|
||||
textarea.placeholder = "One tag per line\nEmpty lines will be ignored";
|
||||
content.appendChild(textarea);
|
||||
|
||||
const btnsDiv = document.createElement("div");
|
||||
btnsDiv.style.cssText = `display: flex; gap: 8px; margin-top: 10px;`;
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.textContent = "💾 Save";
|
||||
saveBtn.style.cssText = `flex: 1; background: #1a3a2a; color: #4CAF50; border: 1px solid #4CAF50; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
saveBtn.onmouseenter = () => saveBtn.style.background = "#2a4a3a";
|
||||
saveBtn.onmouseleave = () => saveBtn.style.background = "#1a3a2a";
|
||||
saveBtn.onclick = async () => {
|
||||
const text = textarea.value;
|
||||
const tags = text.split('\n').map(t => t.trim()).filter(t => t.length > 0);
|
||||
|
||||
if (tags.length === 0) {
|
||||
showRaykoToast("No tags entered", "error", this);
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.textContent = "⏳ Saving...";
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.style.opacity = "0.5";
|
||||
|
||||
try {
|
||||
const res = await fetch("/rayko_lora_loader/add_tags", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName, tags: tags })
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (result.error) {
|
||||
showRaykoToast(result.error, "error", this);
|
||||
saveBtn.textContent = " Save";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.opacity = "1";
|
||||
return;
|
||||
}
|
||||
|
||||
showRaykoToast(`${result.added} tag(s) added`, "success", this);
|
||||
currentData = { ...currentData, trained_words: result.trained_words, source: "manual" };
|
||||
renderChips(currentData);
|
||||
} catch (err) {
|
||||
showRaykoToast("Failed to save tags", "error", this);
|
||||
saveBtn.textContent = "💾 Save";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
btnsDiv.appendChild(saveBtn);
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "❌ Cancel";
|
||||
cancelBtn.style.cssText = `flex: 1; background: #2a2a2a; color: #ccc; border: 1px solid #555; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
cancelBtn.onmouseenter = () => cancelBtn.style.background = "#3a3a3a";
|
||||
cancelBtn.onmouseleave = () => cancelBtn.style.background = "#2a2a2a";
|
||||
cancelBtn.onclick = () => {
|
||||
if (currentData) {
|
||||
renderChips(currentData);
|
||||
}
|
||||
};
|
||||
btnsDiv.appendChild(cancelBtn);
|
||||
|
||||
content.appendChild(btnsDiv);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
fetch("/rayko_lora_loader/get_lora_info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updateContent)
|
||||
.then(data => {
|
||||
currentData = data;
|
||||
renderChips(data);
|
||||
})
|
||||
.catch(err => {
|
||||
content.textContent = "Error loading metadata";
|
||||
content.style.color = "#f44336";
|
||||
|
||||
+187
-17
@@ -1249,7 +1249,54 @@ app.registerExtension({
|
||||
const handleEsc = (e) => { if (e.key === "Escape") { popup.remove(); document.removeEventListener("keydown", handleEsc); } };
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
|
||||
const updateContent = (data) => {
|
||||
let currentData = null;
|
||||
|
||||
const showDeleteConfirmation = (tag, callback) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "rayko-delete-confirm-overlay";
|
||||
overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100002; display: flex; justify-content: center; align-items: center;`;
|
||||
|
||||
const confirmBox = document.createElement("div");
|
||||
confirmBox.style.cssText = `background: #1a1a1a; border: 1px solid #f44336; border-radius: 8px; padding: 20px; max-width: 350px; box-shadow: 0 4px 20px rgba(0,0,0,0.9);`;
|
||||
|
||||
const message = document.createElement("div");
|
||||
message.textContent = `Remove tag "${tag}"?`;
|
||||
message.style.cssText = `color: #fff; font-size: 14px; margin-bottom: 20px; text-align: center;`;
|
||||
|
||||
const btnsDiv = document.createElement("div");
|
||||
btnsDiv.style.cssText = `display: flex; gap: 10px;`;
|
||||
|
||||
const yesBtn = document.createElement("button");
|
||||
yesBtn.textContent = "Yes";
|
||||
yesBtn.style.cssText = `flex: 1; background: #3a1a1a; color: #f44336; border: 1px solid #f44336; border-radius: 4px; padding: 8px; cursor: pointer; font-size: 12px;`;
|
||||
yesBtn.onmouseenter = () => yesBtn.style.background = "#4a2a2a";
|
||||
yesBtn.onmouseleave = () => yesBtn.style.background = "#3a1a1a";
|
||||
yesBtn.onclick = () => {
|
||||
overlay.remove();
|
||||
callback(true);
|
||||
};
|
||||
|
||||
const noBtn = document.createElement("button");
|
||||
noBtn.textContent = "No";
|
||||
noBtn.style.cssText = `flex: 1; background: #2a2a2a; color: #ccc; border: 1px solid #555; border-radius: 4px; padding: 8px; cursor: pointer; font-size: 12px;`;
|
||||
noBtn.onmouseenter = () => noBtn.style.background = "#3a3a3a";
|
||||
noBtn.onmouseleave = () => noBtn.style.background = "#2a2a2a";
|
||||
noBtn.onclick = () => {
|
||||
overlay.remove();
|
||||
callback(false);
|
||||
};
|
||||
|
||||
btnsDiv.appendChild(yesBtn);
|
||||
btnsDiv.appendChild(noBtn);
|
||||
confirmBox.appendChild(message);
|
||||
confirmBox.appendChild(btnsDiv);
|
||||
overlay.appendChild(confirmBox);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
yesBtn.focus();
|
||||
};
|
||||
|
||||
const renderChips = (data) => {
|
||||
content.innerHTML = "";
|
||||
sourceLabel.textContent = `Source: ${data.source || 'unknown'}`;
|
||||
|
||||
@@ -1274,16 +1321,53 @@ app.registerExtension({
|
||||
|
||||
data.trained_words.forEach(word => {
|
||||
const chip = document.createElement("div");
|
||||
chip.textContent = word;
|
||||
chip.style.cssText = `background: #2a3a2a; color: #4CAF50; padding: 4px 10px; border-radius: 12px; font-size: 11px; border: 1px solid #4CAF50; cursor: pointer; user-select: none;`;
|
||||
chip.style.cssText = `background: #2a3a2a; color: #4CAF50; padding: 4px 10px; border-radius: 12px; font-size: 11px; border: 1px solid #4CAF50; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; position: relative;`;
|
||||
|
||||
const textSpan = document.createElement("span");
|
||||
textSpan.textContent = word;
|
||||
textSpan.style.cssText = `flex: 1;`;
|
||||
chip.appendChild(textSpan);
|
||||
|
||||
const deleteBtn = document.createElement("span");
|
||||
deleteBtn.textContent = "✕";
|
||||
deleteBtn.style.cssText = `font-size: 10px; opacity: 0.5; cursor: pointer; padding: 0 2px; line-height: 1;`;
|
||||
deleteBtn.onmouseenter = () => { deleteBtn.style.opacity = "1"; deleteBtn.style.color = "#f44336"; };
|
||||
deleteBtn.onmouseleave = () => { deleteBtn.style.opacity = "0.5"; deleteBtn.style.color = "#4CAF50"; };
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
showDeleteConfirmation(word, (confirmed) => {
|
||||
if (confirmed) {
|
||||
fetch("/rayko/remove_tag", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName, tag: word })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
showRaykoToast(result.error, "error", this);
|
||||
} else {
|
||||
showRaykoToast(`Tag "${word}" removed`, "success", this);
|
||||
currentData = { ...currentData, trained_words: result.trained_words };
|
||||
renderChips(currentData);
|
||||
}
|
||||
})
|
||||
.catch(err => showRaykoToast("Failed to remove tag", "error", this));
|
||||
}
|
||||
});
|
||||
};
|
||||
chip.appendChild(deleteBtn);
|
||||
|
||||
chip.onmouseenter = () => { chip.style.background = "#3a5a3a"; chip.style.borderColor = "#66dd66"; };
|
||||
chip.onmouseleave = () => { chip.style.background = "#2a3a2a"; chip.style.borderColor = "#4CAF50"; };
|
||||
chip.onclick = () => {
|
||||
navigator.clipboard.writeText(word).then(() => {
|
||||
showRaykoToast(`Copied: "${word}"`, "success", this);
|
||||
popup.remove();
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
chip.onclick = (e) => {
|
||||
if (e.target !== deleteBtn && !deleteBtn.contains(e.target)) {
|
||||
navigator.clipboard.writeText(word).then(() => {
|
||||
showRaykoToast(`Copied: "${word}"`, "success", this);
|
||||
popup.remove();
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
}
|
||||
};
|
||||
twContainer.appendChild(chip);
|
||||
});
|
||||
@@ -1303,6 +1387,9 @@ app.registerExtension({
|
||||
}).catch(() => showRaykoToast("Failed to copy", "error", this));
|
||||
};
|
||||
content.appendChild(copyBtn);
|
||||
} else {
|
||||
content.textContent = "No trained words found";
|
||||
content.style.color = "#888";
|
||||
}
|
||||
|
||||
if (data.description) {
|
||||
@@ -1317,16 +1404,22 @@ app.registerExtension({
|
||||
content.appendChild(desc);
|
||||
}
|
||||
|
||||
if (!data.trained_words?.length && !data.description) {
|
||||
content.textContent = "No metadata available";
|
||||
content.style.color = "#888";
|
||||
}
|
||||
|
||||
const actionsDiv = document.createElement("div");
|
||||
actionsDiv.style.cssText = `display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap;`;
|
||||
|
||||
const addTagsBtn = document.createElement("button");
|
||||
const hasTags = data.trained_words && data.trained_words.length > 0;
|
||||
addTagsBtn.textContent = "➕ Add Tags";
|
||||
addTagsBtn.style.cssText = hasTags
|
||||
? `flex: 1; background: #2a2a2a; color: #aaa; border: 1px solid #555; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`
|
||||
: `flex: 1; background: #3a2a1a; color: #fbbf24; border: 1px solid #fbbf24; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
addTagsBtn.onmouseenter = () => addTagsBtn.style.background = hasTags ? "#3a3a3a" : "#4a3a2a";
|
||||
addTagsBtn.onmouseleave = () => addTagsBtn.style.background = hasTags ? "#2a2a2a" : "#3a2a1a";
|
||||
addTagsBtn.onclick = () => showTextarea();
|
||||
actionsDiv.appendChild(addTagsBtn);
|
||||
|
||||
const fetchBtn = document.createElement("button");
|
||||
fetchBtn.textContent = "🌐 Fetch from Civitai";
|
||||
fetchBtn.textContent = " Fetch from Civitai";
|
||||
fetchBtn.style.cssText = `flex: 1; background: #3a2a1a; color: #fbbf24; border: 1px solid #fbbf24; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
fetchBtn.onmouseenter = () => fetchBtn.style.background = "#4a3a2a";
|
||||
fetchBtn.onmouseleave = () => fetchBtn.style.background = "#3a2a1a";
|
||||
@@ -1347,7 +1440,8 @@ app.registerExtension({
|
||||
content.style.color = "#f44336";
|
||||
sourceLabel.textContent = "";
|
||||
} else {
|
||||
updateContent(result);
|
||||
currentData = result;
|
||||
renderChips(result);
|
||||
}
|
||||
} catch (err) {
|
||||
content.textContent = "Network error";
|
||||
@@ -1360,13 +1454,89 @@ app.registerExtension({
|
||||
content.appendChild(actionsDiv);
|
||||
};
|
||||
|
||||
const showTextarea = () => {
|
||||
content.innerHTML = "";
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.style.cssText = `width: 100%; height: 120px; background: #111; color: #fff; border: 1px solid #444; border-radius: 4px; padding: 8px; font-size: 12px; font-family: monospace; resize: vertical; box-sizing: border-box;`;
|
||||
textarea.placeholder = "One tag per line\nEmpty lines will be ignored";
|
||||
content.appendChild(textarea);
|
||||
|
||||
const btnsDiv = document.createElement("div");
|
||||
btnsDiv.style.cssText = `display: flex; gap: 8px; margin-top: 10px;`;
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.textContent = "💾 Save";
|
||||
saveBtn.style.cssText = `flex: 1; background: #1a3a2a; color: #4CAF50; border: 1px solid #4CAF50; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
saveBtn.onmouseenter = () => saveBtn.style.background = "#2a4a3a";
|
||||
saveBtn.onmouseleave = () => saveBtn.style.background = "#1a3a2a";
|
||||
saveBtn.onclick = async () => {
|
||||
const text = textarea.value;
|
||||
const tags = text.split('\n').map(t => t.trim()).filter(t => t.length > 0);
|
||||
|
||||
if (tags.length === 0) {
|
||||
showRaykoToast("No tags entered", "error", this);
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.textContent = "⏳ Saving...";
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.style.opacity = "0.5";
|
||||
|
||||
try {
|
||||
const res = await fetch("/rayko/add_tags", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName, tags: tags })
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (result.error) {
|
||||
showRaykoToast(result.error, "error", this);
|
||||
saveBtn.textContent = " Save";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.opacity = "1";
|
||||
return;
|
||||
}
|
||||
|
||||
showRaykoToast(`${result.added} tag(s) added`, "success", this);
|
||||
currentData = { ...currentData, trained_words: result.trained_words, source: "manual" };
|
||||
renderChips(currentData);
|
||||
} catch (err) {
|
||||
showRaykoToast("Failed to save tags", "error", this);
|
||||
saveBtn.textContent = "💾 Save";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
btnsDiv.appendChild(saveBtn);
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "❌ Cancel";
|
||||
cancelBtn.style.cssText = `flex: 1; background: #2a2a2a; color: #ccc; border: 1px solid #555; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 11px;`;
|
||||
cancelBtn.onmouseenter = () => cancelBtn.style.background = "#3a3a3a";
|
||||
cancelBtn.onmouseleave = () => cancelBtn.style.background = "#2a2a2a";
|
||||
cancelBtn.onclick = () => {
|
||||
if (currentData) {
|
||||
renderChips(currentData);
|
||||
}
|
||||
};
|
||||
btnsDiv.appendChild(cancelBtn);
|
||||
|
||||
content.appendChild(btnsDiv);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
fetch("/rayko/get_lora_info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: loraName })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updateContent)
|
||||
.then(data => {
|
||||
currentData = data;
|
||||
renderChips(data);
|
||||
})
|
||||
.catch(err => {
|
||||
content.textContent = "Error loading metadata";
|
||||
content.style.color = "#f44336";
|
||||
|
||||
Reference in New Issue
Block a user