629 lines
23 KiB
JavaScript
629 lines
23 KiB
JavaScript
import { TEXT, PRESET_STRATEGIES, PRESET_COMPATIBLE_ARCHS, BLOCK_GROUPS, SEMANTIC_COLORS } from "./Constants.js";
|
|
import { ICONS } from "./Icons.js";
|
|
import { createTitanSlider } from "./TitanSlider.js";
|
|
import { sortBlocks, getAnalysisColor, attachMadTooltip, classifyBlock, el, detectPreset, getVectorsForPreset, createAnalysisLegend, normalizeFloat } from "./Utils.js";
|
|
|
|
export class BlockEditorDialog {
|
|
constructor(loraName, arch, stats, currentVectors, snapInterval, onSave) {
|
|
this.loraName = loraName;
|
|
this.arch = arch;
|
|
this.stats = stats || { energy_distribution: {}, block_metadata: {} };
|
|
this.vectors = currentVectors ? { ...currentVectors } : {};
|
|
this.snapInterval = snapInterval || 0.05;
|
|
this.onSave = onSave;
|
|
this.groupCounters = {};
|
|
this.groupKeys = {};
|
|
this.groupStates = {};
|
|
this.presetButtons = {};
|
|
this.maxEnergy = 0;
|
|
Object.values(this.stats.energy_distribution || {}).forEach((v) => (this.maxEnergy = Math.max(this.maxEnergy, v)));
|
|
|
|
this.groupSyncModes = {};
|
|
this._boundKeyHandler = this.handleKey.bind(this);
|
|
if (!this.stats.block_metadata || Object.keys(this.stats.block_metadata).length === 0) {
|
|
this.renderError();
|
|
} else {
|
|
this.render();
|
|
}
|
|
|
|
document.body.appendChild(this.overlay);
|
|
void this.overlay.offsetWidth;
|
|
|
|
requestAnimationFrame(() => {
|
|
this.overlay.classList.add("visible");
|
|
this.modal.classList.add("visible");
|
|
});
|
|
|
|
document.addEventListener("keydown", this._boundKeyHandler);
|
|
}
|
|
|
|
detectSynced(keys) {
|
|
if (!keys || keys.length < 2) return false;
|
|
const firstVal = this.getVal(keys[0]);
|
|
return keys.every((k) => Math.abs(this.getVal(k) - firstVal) < 0.001);
|
|
}
|
|
|
|
renderError() {
|
|
this.modal = el(
|
|
"div",
|
|
{ className: "mad-modal mad-block-editor-modal mad-error-state" },
|
|
el("div", { className: "mad-error-icon", html: TEXT.blockEditor.metadataMissing }),
|
|
el("div", { className: "mad-error-text" }, TEXT.blockEditor.metadataMissingMsg),
|
|
el(
|
|
"button",
|
|
{
|
|
className: "mad-btn",
|
|
onclick: () => this.close(),
|
|
},
|
|
TEXT.close,
|
|
),
|
|
);
|
|
|
|
this.overlay = el(
|
|
"div",
|
|
{
|
|
className: "mad-overlay",
|
|
onclick: (e) => {
|
|
if (e.target === this.overlay) this.close();
|
|
},
|
|
},
|
|
this.modal,
|
|
);
|
|
}
|
|
|
|
render() {
|
|
this.globalCounter = el("div", { className: "mad-label-sm mad-global-counter" });
|
|
const btnClear = el(
|
|
"button",
|
|
{
|
|
className: "mad-btn mad-btn-reset",
|
|
onclick: () => this.bulkSet(0.0),
|
|
},
|
|
TEXT.blockEditor.clear,
|
|
);
|
|
attachMadTooltip(btnClear, TEXT.blockEditor.setZeroTooltip);
|
|
|
|
const btnReset = el(
|
|
"button",
|
|
{
|
|
className: "mad-btn mad-btn-reset",
|
|
onclick: () => this.bulkSet(1.0),
|
|
},
|
|
TEXT.blockEditor.reset,
|
|
);
|
|
attachMadTooltip(btnReset, TEXT.blockEditor.setOneTooltip);
|
|
const header = el(
|
|
"div",
|
|
{ className: "mad-block-header" },
|
|
|
|
el("div", {}, el("div", { className: "mad-block-title" }, TEXT.blockEditor.title), el("div", { className: "mad-block-subtitle" }, TEXT.blockEditor.subtitle, this.loraName.split(/[/\\]/).pop(), el("span", { className: "mad-arch-tag" }, this.arch || "UNKNOWN")), this.globalCounter),
|
|
|
|
el("div", { className: "mad-block-actions" }, btnClear, btnReset),
|
|
);
|
|
this.content = el("div", { className: "mad-block-content" });
|
|
this.presetWrapper = el("div", { className: "mad-block-preset-wrapper" });
|
|
|
|
const archUpper = (this.arch || "").toUpperCase();
|
|
const isSupported = PRESET_COMPATIBLE_ARCHS.some((prefix) => archUpper.includes(prefix));
|
|
|
|
if (!isSupported) {
|
|
this.presetWrapper.classList.add("mad-disabled-content");
|
|
}
|
|
const toolbar = el(
|
|
"div",
|
|
{ className: "mad-block-toolbar" },
|
|
el(
|
|
"button",
|
|
{
|
|
className: "mad-text-action",
|
|
onclick: () => this.toggleAllGroups(true),
|
|
},
|
|
TEXT.blockEditor.expandAll,
|
|
),
|
|
el(
|
|
"button",
|
|
{
|
|
className: "mad-text-action",
|
|
onclick: () => this.toggleAllGroups(false),
|
|
},
|
|
TEXT.blockEditor.collapseAll,
|
|
),
|
|
);
|
|
const stickyHeader = el("div", { className: "mad-block-sticky-header" }, this.presetWrapper, toolbar);
|
|
this.renderPresetBar(isSupported);
|
|
this.listContainer = el("div", { className: "mad-block-list-container" });
|
|
this.content.appendChild(this.listContainer);
|
|
|
|
this.renderList();
|
|
this.updateGlobalCounter();
|
|
const footerLegend = el("div", { className: "mad-block-footer-legend mad-scale-12" }, createAnalysisLegend());
|
|
const footer = el(
|
|
"div",
|
|
{ className: "mad-block-footer" },
|
|
footerLegend,
|
|
el(
|
|
"div",
|
|
{ className: "mad-block-footer-actions" },
|
|
el(
|
|
"button",
|
|
{
|
|
className: "mad-btn",
|
|
onclick: () => this.close(),
|
|
},
|
|
TEXT.blockEditor.cancel,
|
|
),
|
|
el(
|
|
"button",
|
|
{
|
|
className: "mad-btn mad-btn-primary",
|
|
onclick: () => {
|
|
const cleaned = {};
|
|
let count = 0;
|
|
for (const [k, v] of Object.entries(this.vectors)) {
|
|
if (Math.abs(v - 1.0) > 0.001) {
|
|
cleaned[k] = v;
|
|
count++;
|
|
}
|
|
}
|
|
const payload = count > 0 ? cleaned : null;
|
|
this.onSave(payload);
|
|
this.close();
|
|
},
|
|
},
|
|
TEXT.blockEditor.apply,
|
|
),
|
|
),
|
|
);
|
|
|
|
this.modal = el("div", { className: "mad-modal mad-block-editor-modal" }, header, stickyHeader, this.content, footer);
|
|
|
|
this.overlay = el(
|
|
"div",
|
|
{
|
|
className: "mad-overlay",
|
|
onmousedown: (e) => {
|
|
this.mouseDownOnOverlay = e.target === this.overlay;
|
|
},
|
|
onclick: (e) => {
|
|
if (e.target === this.overlay && this.mouseDownOnOverlay) this.close();
|
|
this.mouseDownOnOverlay = false;
|
|
},
|
|
},
|
|
this.modal,
|
|
);
|
|
}
|
|
|
|
renderPresetBar(isEnabled = true) {
|
|
this.presetWrapper.innerHTML = "";
|
|
this.presetButtons = {};
|
|
|
|
const labelText = isEnabled ? TEXT.blockEditor.presetLabel : `${TEXT.blockEditor.presetLabel} (Unsupported Architecture)`;
|
|
|
|
const label = el("div", { className: "mad-label-sm mad-cursor-help" }, labelText);
|
|
attachMadTooltip(label, TEXT.blockEditor.strategyTooltip);
|
|
|
|
const container = el("div", { className: "mad-seg-control" });
|
|
|
|
const shortLabels = TEXT.blockEditor.presetShortLabels;
|
|
|
|
const currentPreset = this.detectPreset();
|
|
|
|
Object.entries(PRESET_STRATEGIES).forEach(([key, strat]) => {
|
|
const btn = el(
|
|
"div",
|
|
{
|
|
className: `mad-seg-btn mad-seg-text-btn ${currentPreset === key ? "active" : ""}`,
|
|
onclick: () => this.applyPreset(key),
|
|
},
|
|
shortLabels[key] || strat.label,
|
|
);
|
|
|
|
let tooltipHtml = `<strong>${strat.label}</strong><br/>${strat.desc}`;
|
|
attachMadTooltip(btn, tooltipHtml);
|
|
|
|
this.presetButtons[key] = btn;
|
|
container.appendChild(btn);
|
|
});
|
|
|
|
this.presetWrapper.appendChild(label);
|
|
this.presetWrapper.appendChild(container);
|
|
}
|
|
|
|
updatePresetUI() {
|
|
const current = this.detectPreset();
|
|
Object.entries(this.presetButtons).forEach(([key, btn]) => {
|
|
if (key === current) btn.classList.add("active");
|
|
else btn.classList.remove("active");
|
|
});
|
|
}
|
|
|
|
toggleAllGroups(expand) {
|
|
Object.keys(this.groupStates).forEach((k) => {
|
|
this.groupStates[k] = expand;
|
|
});
|
|
this.renderList();
|
|
}
|
|
detectPreset() {
|
|
return detectPreset(this.vectors, this.stats, this.arch);
|
|
}
|
|
|
|
getVal(key) {
|
|
return this.vectors[key] !== undefined ? normalizeFloat(this.vectors[key]) : 1.0;
|
|
}
|
|
getBlockBaseColor(key) {
|
|
const meta = this.stats.block_metadata[key];
|
|
const tag = meta ? meta.tag : classifyBlock(key, this.arch);
|
|
return SEMANTIC_COLORS[tag] || SEMANTIC_COLORS["OTHER"];
|
|
}
|
|
applyPreset(presetKey) {
|
|
const newVectors = getVectorsForPreset(presetKey, this.stats, this.arch);
|
|
this.vectors = { ...newVectors };
|
|
this.groupSyncModes = {};
|
|
this.renderList();
|
|
this.updatePresetUI();
|
|
this.updateGlobalCounter();
|
|
}
|
|
|
|
bulkSet(val) {
|
|
const allBlocks = Object.keys(this.stats.energy_distribution || {});
|
|
allBlocks.forEach((key) => {
|
|
if (Math.abs(val - 1.0) < 0.001) delete this.vectors[key];
|
|
else this.vectors[key] = val;
|
|
});
|
|
this.groupSyncModes = {};
|
|
this.renderList();
|
|
this.updatePresetUI();
|
|
this.updateGlobalCounter();
|
|
}
|
|
|
|
updateGlobalCounter() {
|
|
if (!this.globalCounter) return;
|
|
|
|
const allBlocks = Object.keys(this.stats.energy_distribution || {});
|
|
const total = allBlocks.length;
|
|
let active = 0;
|
|
|
|
allBlocks.forEach((k) => {
|
|
if (this.getVal(k) > 0.0) active++;
|
|
});
|
|
|
|
this.globalCounter.textContent = TEXT.blockEditor.globalActive.replace("{count}", active).replace("{total}", total);
|
|
|
|
this.globalCounter.style.opacity = active > 0 ? "1" : "0.5";
|
|
}
|
|
|
|
renderList() {
|
|
document.querySelectorAll(".mad-tooltip").forEach((el) => el.remove());
|
|
|
|
this.listContainer.innerHTML = "";
|
|
this.groupCounters = {};
|
|
this.groupKeys = {};
|
|
|
|
const allBlocks = Object.keys(this.stats.energy_distribution || {});
|
|
const sortedBlocks = sortBlocks(allBlocks);
|
|
const groups = {};
|
|
if (BLOCK_GROUPS && BLOCK_GROUPS.length > 0) {
|
|
BLOCK_GROUPS.forEach((g) => (groups[g.label] = []));
|
|
} else {
|
|
groups[TEXT.fallbackBlockGroups.clip] = [];
|
|
groups[TEXT.fallbackBlockGroups.input] = [];
|
|
groups[TEXT.fallbackBlockGroups.middle] = [];
|
|
groups[TEXT.fallbackBlockGroups.output] = [];
|
|
groups[TEXT.fallbackBlockGroups.layers] = [];
|
|
groups[TEXT.fallbackBlockGroups.aux] = [];
|
|
}
|
|
|
|
sortedBlocks.forEach((key) => {
|
|
const meta = this.stats.block_metadata[key];
|
|
if (meta && meta.group) {
|
|
const groupDef = BLOCK_GROUPS.find((g) => g.id === meta.group);
|
|
const label = groupDef ? groupDef.label : TEXT.fallbackBlockGroups.aux;
|
|
if (!groups[label]) groups[label] = [];
|
|
groups[label].push(key);
|
|
} else {
|
|
const k = key.toLowerCase();
|
|
if (k.startsWith("clip") || k.startsWith("te_")) groups[TEXT.fallbackBlockGroups.clip].push(key);
|
|
else if (k.match(/(?:^|_)(input|down|double)/i)) groups[TEXT.fallbackBlockGroups.input].push(key);
|
|
else if (k.match(/(?:^|_)(middle|mid|joint)/i)) groups[TEXT.fallbackBlockGroups.middle].push(key);
|
|
else if (k.match(/(?:^|_)(output|up|single)/i)) groups[TEXT.fallbackBlockGroups.output].push(key);
|
|
else if (k.startsWith("layers")) groups[TEXT.fallbackBlockGroups.layers].push(key);
|
|
else groups[TEXT.fallbackBlockGroups.aux].push(key);
|
|
}
|
|
});
|
|
|
|
Object.entries(groups).forEach(([groupName, keys]) => {
|
|
if (keys.length === 0) return;
|
|
|
|
this.groupKeys[groupName] = keys;
|
|
|
|
if (this.groupStates[groupName] === undefined) {
|
|
this.groupStates[groupName] = true;
|
|
}
|
|
const groupColor = keys.length > 0 ? this.getBlockBaseColor(keys[0]) : "#9e9e9e";
|
|
|
|
const arrow = el("span", {
|
|
className: "mad-arrow-icon",
|
|
html: this.groupStates[groupName] ? ICONS.arrowDown : ICONS.arrowRight,
|
|
});
|
|
const colorDot = el("span", {
|
|
className: "mad-group-dot",
|
|
style: { backgroundColor: groupColor },
|
|
});
|
|
|
|
const counter = el("span", { className: "mad-group-counter" }, "...");
|
|
this.groupCounters[groupName] = counter;
|
|
|
|
const headerContent = [colorDot, arrow, groupName, counter];
|
|
const canSync = keys.length > 1;
|
|
if (canSync && this.groupSyncModes[groupName] === undefined) {
|
|
this.groupSyncModes[groupName] = this.detectSynced(keys);
|
|
}
|
|
|
|
const isSynced = canSync && this.groupSyncModes[groupName];
|
|
|
|
if (canSync) {
|
|
const syncBtn = el("button", {
|
|
className: "mad-btn mad-btn-reset mad-sync-btn",
|
|
style: {
|
|
color: isSynced ? "var(--accent)" : "var(--text-muted)",
|
|
borderColor: isSynced ? "var(--accent)" : "transparent",
|
|
display: this.groupStates[groupName] ? "inline-flex" : "none",
|
|
},
|
|
onclick: (e) => {
|
|
e.stopPropagation();
|
|
|
|
const newState = !this.groupSyncModes[groupName];
|
|
this.groupSyncModes[groupName] = newState;
|
|
if (newState) {
|
|
const alignVal = this.getVal(keys[0]);
|
|
keys.forEach((k) => {
|
|
if (Math.abs(alignVal - 1.0) < 0.001) delete this.vectors[k];
|
|
else this.vectors[k] = alignVal;
|
|
});
|
|
this.updateGlobalCounter();
|
|
this.updatePresetUI();
|
|
}
|
|
|
|
this.renderList();
|
|
},
|
|
html: isSynced ? `${ICONS.arrowRight} ${TEXT.blockEditor.synced}` : `${TEXT.blockEditor.split} ${ICONS.arrowDown}`,
|
|
});
|
|
|
|
attachMadTooltip(syncBtn, isSynced ? TEXT.blockEditor.syncedTooltip : TEXT.blockEditor.splitTooltip);
|
|
|
|
headerContent.push(syncBtn);
|
|
}
|
|
|
|
const header = el(
|
|
"div",
|
|
{
|
|
className: "mad-block-group-header",
|
|
},
|
|
...headerContent,
|
|
);
|
|
|
|
const rowContainer = el("div", {
|
|
style: { display: this.groupStates[groupName] ? "block" : "none" },
|
|
});
|
|
|
|
header.onclick = (e) => {
|
|
if (e.target.tagName === "BUTTON") return;
|
|
const isExpanded = !this.groupStates[groupName];
|
|
this.groupStates[groupName] = isExpanded;
|
|
rowContainer.style.display = isExpanded ? "block" : "none";
|
|
arrow.innerHTML = isExpanded ? ICONS.arrowDown : ICONS.arrowRight;
|
|
const syncBtn = header.querySelector(".mad-btn-reset");
|
|
if (syncBtn) {
|
|
syncBtn.style.display = isExpanded ? "inline-flex" : "none";
|
|
}
|
|
};
|
|
|
|
this.listContainer.appendChild(header);
|
|
this.listContainer.appendChild(rowContainer);
|
|
if (isSynced) {
|
|
const commonVal = this.getVal(keys[0]);
|
|
rowContainer.appendChild(this.createMasterRow(groupName, keys, commonVal));
|
|
} else {
|
|
keys.forEach((key) => {
|
|
rowContainer.appendChild(this.createBlockRow(key));
|
|
});
|
|
}
|
|
|
|
this.updateGroupCounter(groupName, keys);
|
|
});
|
|
}
|
|
|
|
updateGroupCounter(groupName, keys) {
|
|
const counterEl = this.groupCounters[groupName];
|
|
if (counterEl) {
|
|
const gTotal = keys.length;
|
|
let gActive = 0;
|
|
|
|
keys.forEach((k) => {
|
|
const val = this.getVal(k);
|
|
if (val > 0.0) gActive++;
|
|
});
|
|
|
|
let html = TEXT.blockEditor.groupCount.replace("{name}", "").replace("{count}", `${gActive} / ${gTotal}`);
|
|
|
|
counterEl.innerHTML = html;
|
|
|
|
const opacity = gActive > 0 ? "1" : "0.5";
|
|
counterEl.style.opacity = opacity;
|
|
|
|
if (counterEl.firstElementChild) {
|
|
counterEl.firstElementChild.style.opacity = opacity;
|
|
}
|
|
}
|
|
}
|
|
|
|
createMasterRow(groupName, keys, currentVal) {
|
|
const groupColor = keys.length > 0 ? this.getBlockBaseColor(keys[0]) : "#9e9e9e";
|
|
const colorWithOpacity = getAnalysisColor(currentVal, this.stats.block_metadata[keys[0]]?.tag || classifyBlock(keys[0], this.arch));
|
|
|
|
const info = el(
|
|
"div",
|
|
{ className: "mad-block-info" },
|
|
el(
|
|
"span",
|
|
{
|
|
className: "mad-block-label",
|
|
style: { borderLeft: `3px solid ${colorWithOpacity}` },
|
|
},
|
|
TEXT.blockEditor.masterGroup,
|
|
),
|
|
el("div", { className: "mad-desc-text" }, TEXT.blockEditor.masterGroupDesc.replace("{count}", keys.length)),
|
|
);
|
|
|
|
const slider = createTitanSlider(
|
|
"",
|
|
0.0,
|
|
2.0,
|
|
this.snapInterval,
|
|
currentVal,
|
|
(val) => {
|
|
const normVal = normalizeFloat(val);
|
|
keys.forEach((k) => {
|
|
if (Math.abs(normVal - 1.0) < 0.001) delete this.vectors[k];
|
|
else this.vectors[k] = normVal;
|
|
});
|
|
this.updateGroupCounter(groupName, keys);
|
|
this.updatePresetUI();
|
|
this.updateGlobalCounter();
|
|
|
|
if (val === 0) row.style.opacity = "0.5";
|
|
else row.style.opacity = "1";
|
|
},
|
|
false,
|
|
false,
|
|
TEXT.blockEditor.masterGroup,
|
|
null,
|
|
null,
|
|
true,
|
|
);
|
|
|
|
const row = el(
|
|
"div",
|
|
{
|
|
className: "mad-block-row",
|
|
style: {
|
|
borderLeft: `3px solid ${colorWithOpacity}`,
|
|
marginLeft: "2px",
|
|
},
|
|
},
|
|
info,
|
|
el("div", { className: "mad-block-control" }, slider),
|
|
);
|
|
|
|
if (currentVal === 0) row.classList.add("mad-dimmed");
|
|
return row;
|
|
}
|
|
|
|
createBlockRow(key) {
|
|
const energyVal = this.stats.energy_distribution[key] || 0;
|
|
const energyRatio = this.maxEnergy > 0 ? energyVal / this.maxEnergy : 0;
|
|
const currentVal = this.getVal(key);
|
|
const meta = this.stats.block_metadata[key];
|
|
const tag = meta ? meta.tag : classifyBlock(key, this.arch);
|
|
const color = getAnalysisColor(currentVal, tag);
|
|
|
|
const info = el(
|
|
"div",
|
|
{ className: "mad-block-info" },
|
|
el(
|
|
"span",
|
|
{
|
|
className: "mad-block-label",
|
|
style: { borderLeft: `3px solid ${color}` },
|
|
},
|
|
key,
|
|
),
|
|
el(
|
|
"div",
|
|
{ className: "mad-block-energy-bar" },
|
|
el("div", {
|
|
className: "mad-block-energy-fill",
|
|
style: {
|
|
width: `${Math.max(5, energyRatio * 100)}%`,
|
|
backgroundColor: color,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
attachMadTooltip(info, TEXT.blockTooltips.blockInfo.replace("{ratio}", (energyRatio * 100).toFixed(1)));
|
|
|
|
const sliderTip = TEXT.blockTooltips.blockControl.replace("{key}", key).replace("{ratio}", (energyRatio * 100).toFixed(1));
|
|
|
|
const slider = createTitanSlider(
|
|
"",
|
|
0.0,
|
|
2.0,
|
|
this.snapInterval,
|
|
currentVal,
|
|
(val) => {
|
|
const normVal = normalizeFloat(val);
|
|
if (Math.abs(normVal - 1.0) < 0.001) delete this.vectors[key];
|
|
else this.vectors[key] = normVal;
|
|
for (const [gName, gKeys] of Object.entries(this.groupKeys)) {
|
|
if (gKeys.includes(key)) {
|
|
this.updateGroupCounter(gName, gKeys);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.updatePresetUI();
|
|
this.updateGlobalCounter();
|
|
|
|
if (val === 0) row.style.opacity = "0.5";
|
|
else row.style.opacity = "1";
|
|
},
|
|
false,
|
|
false,
|
|
sliderTip,
|
|
null,
|
|
null,
|
|
true,
|
|
);
|
|
|
|
const row = el(
|
|
"div",
|
|
{
|
|
className: "mad-block-row",
|
|
style: {
|
|
borderLeft: `3px solid ${color}`,
|
|
marginLeft: "2px",
|
|
},
|
|
},
|
|
info,
|
|
el("div", { className: "mad-block-control" }, slider),
|
|
);
|
|
if (currentVal === 0) row.classList.add("mad-dimmed");
|
|
else row.classList.remove("mad-dimmed");
|
|
|
|
return row;
|
|
}
|
|
|
|
handleKey(e) {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
close() {
|
|
document.querySelectorAll(".mad-tooltip").forEach((el) => el.remove());
|
|
this.overlay.style.pointerEvents = "none";
|
|
this.overlay.classList.remove("visible");
|
|
this.modal.classList.remove("visible");
|
|
|
|
setTimeout(() => {
|
|
if (document.body.contains(this.overlay)) document.body.removeChild(this.overlay);
|
|
}, 300);
|
|
|
|
document.removeEventListener("keydown", this._boundKeyHandler);
|
|
}
|
|
}
|