598 lines
25 KiB
JavaScript
598 lines
25 KiB
JavaScript
(() => {
|
|
const openButton = document.querySelector("[data-ai-tools-open]");
|
|
const modal = document.querySelector("[data-ai-tools-modal]");
|
|
const list = modal?.querySelector("[data-ai-tools-list]");
|
|
const source = modal?.querySelector("[data-ai-tools-source]");
|
|
const refresh = modal?.querySelector("[data-ai-tools-refresh]");
|
|
const readmeModal = document.querySelector("[data-ai-tool-readme-modal]");
|
|
const readmeTitle = readmeModal?.querySelector("[data-ai-tool-readme-title]");
|
|
const readmeBody = readmeModal?.querySelector("[data-ai-tool-readme]");
|
|
const settingsModal = document.querySelector("[data-ai-tool-settings-modal]");
|
|
const settingsTitle = settingsModal?.querySelector("[data-ai-tool-settings-title]");
|
|
const settingsForm = settingsModal?.querySelector("[data-ai-tool-settings-form]");
|
|
const settingsCustom = settingsModal?.querySelector("[data-ai-tool-settings-custom]");
|
|
const settingsFields = settingsModal?.querySelector("[data-ai-tool-settings-fields]");
|
|
const settingsSave = settingsModal?.querySelector("[data-ai-tool-settings-save]");
|
|
const settingsReset = settingsModal?.querySelector("[data-ai-tool-settings-reset]");
|
|
const diagnostics = modal?.querySelector("[data-ai-tool-diagnostics]");
|
|
const diagnosticRole = diagnostics?.querySelector("[data-ai-tool-diagnostic-role]");
|
|
const diagnosticOrigin = diagnostics?.querySelector("[data-ai-tool-diagnostic-origin]");
|
|
const diagnosticRefresh = diagnostics?.querySelector("[data-ai-tool-diagnostic-refresh]");
|
|
const diagnosticResults = diagnostics?.querySelector("[data-ai-tool-diagnostic-results]");
|
|
const promptPreview = diagnostics?.querySelector("[data-ai-tool-prompt-preview]");
|
|
if (!openButton || !modal || !list || !source || !readmeModal || !readmeTitle || !readmeBody ||
|
|
!settingsModal || !settingsTitle || !settingsForm || !settingsCustom || !settingsFields ||
|
|
!settingsSave || !settingsReset ||
|
|
!diagnostics || !diagnosticRole || !diagnosticOrigin || !diagnosticResults || !promptPreview) return;
|
|
|
|
let loading = false;
|
|
let activeSettingsTool = null;
|
|
let activeSettingsPayload = null;
|
|
|
|
const setOpen = (target, open) => {
|
|
target.classList.toggle("is-open", open);
|
|
target.setAttribute("aria-hidden", String(!open));
|
|
};
|
|
|
|
const loadTools = async (force = false) => {
|
|
if (loading) return;
|
|
loading = true;
|
|
list.replaceChildren(message("Loading AI tool plugins..."));
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_ai/api/tools${force ? "?refresh=1" : ""}`, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Unable to load AI tools.");
|
|
source.textContent = `${payload.repository} · ${payload.branch} · checked ${payload.checked_at ? new Date(payload.checked_at).toLocaleString() : "never"}${payload.cached ? " · cached" : ""}${payload.stale ? " · stale" : ""}`;
|
|
source.classList.toggle("error", Boolean(payload.error));
|
|
if (payload.error) source.title = payload.error;
|
|
renderTools(payload.tools || []);
|
|
} catch (error) {
|
|
list.replaceChildren(message(error.message, true));
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
};
|
|
|
|
const renderTools = (tools) => {
|
|
list.replaceChildren();
|
|
if (!tools.length) {
|
|
list.append(message("No local or remote Lumi AI tool plugins were found."));
|
|
return;
|
|
}
|
|
for (const tool of tools) list.append(renderTool(tool));
|
|
};
|
|
|
|
const renderTool = (tool) => {
|
|
const row = document.createElement("article");
|
|
row.className = "ai-tool-row";
|
|
const summary = document.createElement("div");
|
|
summary.className = "ai-tool-summary";
|
|
const identity = document.createElement("div");
|
|
identity.className = "ai-tool-identity";
|
|
const name = document.createElement("strong");
|
|
name.textContent = tool.display_name || tool.tool_id;
|
|
const id = document.createElement("span");
|
|
id.textContent = tool.tool_id;
|
|
identity.append(name, id);
|
|
|
|
const versions = document.createElement("div");
|
|
versions.className = "ai-tool-versions";
|
|
versions.append(
|
|
badge(tool.installed ? "Installed" : "Remote", tool.installed ? "installed" : ""),
|
|
badge(tool.enabled ? "Enabled" : "Disabled", tool.enabled ? "installed" : ""),
|
|
textPair("Local", tool.local_version || "-"),
|
|
textPair("Remote", tool.remote_version || "-"),
|
|
badge(tool.remote_missing ? "Remote missing" : tool.update_available ? "Update available" : "Current", tool.update_available ? "warning" : "")
|
|
);
|
|
|
|
const scope = document.createElement("div");
|
|
scope.className = "ai-tool-scope";
|
|
scope.textContent = `${tool.primary_type || "general"} · ${tool.primary_scope || "unspecified"}`;
|
|
|
|
const actions = document.createElement("div");
|
|
actions.className = "ai-tool-actions";
|
|
const expand = button("Details", "subtle");
|
|
const details = renderDetails(tool);
|
|
expand.addEventListener("click", () => {
|
|
details.hidden = !details.hidden;
|
|
expand.textContent = details.hidden ? "Details" : "Hide details";
|
|
});
|
|
const inspect = button("Inspect", "subtle");
|
|
inspect.addEventListener("click", () => inspectReadme(tool));
|
|
const settings = button("Settings", "subtle");
|
|
settings.disabled = !tool.has_settings;
|
|
settings.title = tool.has_settings ? "" : "This tool does not expose configurable settings.";
|
|
settings.addEventListener("click", () => openSettings(tool));
|
|
const enable = button(tool.enabled ? "Disable" : "Enable", tool.enabled ? "subtle" : "");
|
|
enable.disabled = tool.installed && !tool.local_valid;
|
|
enable.addEventListener("click", () => runAction(tool, tool.enabled ? "disable" : "enable", enable));
|
|
const update = button("Update", tool.update_available ? "update" : "subtle");
|
|
update.disabled = !tool.update_enabled;
|
|
update.title = !tool.installed ? "Install the tool before updating." : tool.remote_missing ? "This tool is missing remotely." : "";
|
|
update.addEventListener("click", () => runAction(tool, "update", update));
|
|
actions.append(expand, inspect, settings, enable, update);
|
|
if (tool.installed) actions.append(deleteForm(tool));
|
|
|
|
summary.append(identity, versions, scope, actions);
|
|
row.append(summary, details);
|
|
return row;
|
|
};
|
|
|
|
const renderDetails = (tool) => {
|
|
const details = document.createElement("div");
|
|
details.className = "ai-tool-details";
|
|
details.hidden = true;
|
|
details.append(
|
|
detail("Description", tool.description || "No description."),
|
|
detail("Capabilities", joinValue(tool.capabilities)),
|
|
detail("Limitations", joinValue(tool.limitations)),
|
|
detail("Scope", joinValue(tool.scope)),
|
|
detail("Permissions", joinValue(tool.permissions)),
|
|
detail("Dependencies", joinValue(tool.dependencies)),
|
|
detail("Required plugins", joinValue(tool.required_plugins)),
|
|
detail("Required platforms", joinValue(tool.required_platforms)),
|
|
detail("Dependency availability", dependencyStatus(tool.dependency_status)),
|
|
detail("Risk / confirmation", `${tool.risk_level || "sensitive"} / ${tool.confirmation_required === false ? "not required" : "required"}`),
|
|
detail("Runtime", `${tool.runtime_state || "unknown"}${tool.runtime_message ? `: ${tool.runtime_message}` : ""}`),
|
|
detail("Registered definitions", joinValue(tool.registered_tools)),
|
|
detail("Install status", tool.local_error || tool.remote_error || (tool.remote_missing ? "Installed locally; missing from remote repository." : "Ready"))
|
|
);
|
|
return details;
|
|
};
|
|
|
|
const loadDiagnostics = async () => {
|
|
diagnosticResults.replaceChildren(message("Evaluating registered tools..."));
|
|
promptPreview.textContent = "Loading...";
|
|
diagnosticRefresh.disabled = true;
|
|
try {
|
|
const query = new URLSearchParams({
|
|
role: diagnosticRole.value,
|
|
origin: diagnosticOrigin.value
|
|
});
|
|
const response = await fetch(`/plugins/lumi_ai/api/tools-diagnostics?${query}`, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Tool diagnostics are unavailable.");
|
|
renderDiagnostics(payload);
|
|
} catch (error) {
|
|
diagnosticResults.replaceChildren(message(error.message, true));
|
|
promptPreview.textContent = "(unavailable)";
|
|
} finally {
|
|
diagnosticRefresh.disabled = false;
|
|
}
|
|
};
|
|
|
|
const renderDiagnostics = (payload) => {
|
|
diagnosticResults.replaceChildren();
|
|
if (!payload.plugins?.length) {
|
|
diagnosticResults.append(message("No installed AI tool plugins were discovered."));
|
|
}
|
|
for (const plugin of payload.plugins || []) {
|
|
const row = document.createElement("div");
|
|
row.className = `ai-tool-diagnostic-row ${plugin.prompt_exposed ? "exposed" : "hidden"}`;
|
|
const decisions = (plugin.decisions || []).map((decision) =>
|
|
`${decision.tool.tool_id}: ${decision.exposed ? "exposed" : decision.reason}`
|
|
).join(" | ");
|
|
row.textContent = [
|
|
plugin.tool_id,
|
|
`state=${plugin.state}`,
|
|
`enabled=${plugin.enabled}`,
|
|
`registered=${(plugin.registered_tools || []).join(", ") || "none"}`,
|
|
plugin.prompt_exposed ? "prompt=exposed" : `prompt=hidden (${plugin.hidden_reason || "unknown"})`,
|
|
plugin.message || "",
|
|
plugin.runtime_details ? `details=${JSON.stringify(plugin.runtime_details)}` : "",
|
|
decisions
|
|
].filter(Boolean).join(" · ");
|
|
diagnosticResults.append(row);
|
|
}
|
|
promptPreview.textContent = payload.prompt_preview || "ALLOWED TOOLS:\n(none)";
|
|
};
|
|
|
|
const runAction = async (tool, action, control) => {
|
|
control.disabled = true;
|
|
const original = control.textContent;
|
|
control.textContent = action === "enable" ? "Enabling..." : action === "disable" ? "Disabling..." : "Updating...";
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_ai/tools/${encodeURIComponent(tool.tool_id)}/${action}`, {
|
|
method: "POST",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || `${action} failed.`);
|
|
await loadTools(false);
|
|
} catch (error) {
|
|
window.alert(error.message);
|
|
control.disabled = false;
|
|
control.textContent = original;
|
|
}
|
|
};
|
|
|
|
const deleteForm = (tool) => {
|
|
const form = document.createElement("form");
|
|
form.method = "post";
|
|
form.action = `/plugins/lumi_ai/tools/${encodeURIComponent(tool.tool_id)}/delete`;
|
|
form.dataset.confirmMode = "modal";
|
|
form.dataset.confirmTitle = `Delete ${tool.display_name || tool.tool_id}?`;
|
|
form.dataset.confirmText = "The installed AI tool files will be removed. Shared Lumi AI data and unrelated plugins are not affected.";
|
|
const remove = button("Delete", "danger");
|
|
remove.type = "submit";
|
|
form.append(remove);
|
|
return form;
|
|
};
|
|
|
|
const inspectReadme = async (tool) => {
|
|
readmeTitle.textContent = `${tool.display_name || tool.tool_id} documentation`;
|
|
readmeBody.replaceChildren(message("Loading readme.md..."));
|
|
setOpen(readmeModal, true);
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_ai/api/tools/${encodeURIComponent(tool.tool_id)}/readme`, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Unable to load readme.md.");
|
|
renderMarkdown(readmeBody, payload.markdown);
|
|
} catch (error) {
|
|
readmeBody.replaceChildren(message(error.message, true));
|
|
}
|
|
};
|
|
|
|
const openSettings = async (tool) => {
|
|
activeSettingsTool = tool;
|
|
activeSettingsPayload = null;
|
|
settingsTitle.textContent = `${tool.display_name || tool.tool_id} settings`;
|
|
settingsCustom.replaceChildren();
|
|
settingsFields.replaceChildren(message("Loading settings..."));
|
|
setOpen(settingsModal, true);
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_ai/api/tools/${encodeURIComponent(tool.tool_id)}/settings`, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Unable to load tool settings.");
|
|
renderSettings(payload);
|
|
} catch (error) {
|
|
settingsFields.replaceChildren(message(error.message, true));
|
|
}
|
|
};
|
|
|
|
const renderSettings = (payload) => {
|
|
activeSettingsPayload = payload;
|
|
renderCustomSettings(payload);
|
|
settingsFields.replaceChildren();
|
|
for (const [key, field] of Object.entries(payload.schema || {})) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = `ai-tool-setting${["string_list", "multi_select"].includes(field.type) ? " wide" : ""}`;
|
|
const label = document.createElement("label");
|
|
label.textContent = field.label || key;
|
|
const control = settingsControl(key, field, payload.values?.[key], payload.configured_secrets?.[key]);
|
|
label.htmlFor = control.id || "";
|
|
wrapper.append(label, control);
|
|
if (field.description) {
|
|
const hint = document.createElement("p");
|
|
hint.className = "hint";
|
|
hint.textContent = field.description;
|
|
wrapper.append(hint);
|
|
}
|
|
settingsFields.append(wrapper);
|
|
}
|
|
};
|
|
|
|
const renderCustomSettings = async (payload) => {
|
|
settingsCustom.replaceChildren();
|
|
if (!payload.ui?.html) return;
|
|
for (const href of payload.ui.styles || []) loadStyle(href);
|
|
settingsCustom.innerHTML = payload.ui.html;
|
|
await Promise.all((payload.ui.scripts || []).map(loadScript));
|
|
window.dispatchEvent(new CustomEvent("lumi-ai-tool-settings-open", {
|
|
detail: {
|
|
toolId: activeSettingsTool?.tool_id,
|
|
payload,
|
|
root: settingsCustom
|
|
}
|
|
}));
|
|
};
|
|
|
|
const loadStyle = (href) => {
|
|
if (document.querySelector(`link[data-ai-tool-asset="${CSS.escape(href)}"]`)) return;
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = href;
|
|
link.dataset.aiToolAsset = href;
|
|
document.head.append(link);
|
|
};
|
|
|
|
const loadScript = (src) => {
|
|
const existing = document.querySelector(`script[data-ai-tool-asset="${CSS.escape(src)}"]`);
|
|
if (existing) return Promise.resolve();
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement("script");
|
|
script.src = src;
|
|
script.dataset.aiToolAsset = src;
|
|
script.addEventListener("load", resolve, { once: true });
|
|
script.addEventListener("error", () => reject(new Error(`Unable to load ${src}.`)), { once: true });
|
|
document.head.append(script);
|
|
});
|
|
};
|
|
|
|
const settingsControl = (key, field, value, configuredSecret) => {
|
|
if (field.type === "boolean") {
|
|
const input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.name = key;
|
|
input.id = `ai-tool-setting-${key}`;
|
|
input.checked = value === true;
|
|
return input;
|
|
}
|
|
if (field.type === "multi_select") {
|
|
const group = document.createElement("div");
|
|
group.className = "check-grid";
|
|
group.dataset.settingName = key;
|
|
for (const option of field.options || []) {
|
|
const label = document.createElement("label");
|
|
const input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.value = option;
|
|
input.checked = Array.isArray(value) && value.includes(option);
|
|
label.append(input, document.createTextNode(option));
|
|
group.append(label);
|
|
}
|
|
return group;
|
|
}
|
|
if (field.type === "string_list") {
|
|
const textarea = document.createElement("textarea");
|
|
textarea.name = key;
|
|
textarea.id = `ai-tool-setting-${key}`;
|
|
textarea.rows = field.rows || 3;
|
|
textarea.value = Array.isArray(value) ? value.join("\n") : "";
|
|
return textarea;
|
|
}
|
|
if (field.type === "enum") {
|
|
const select = document.createElement("select");
|
|
select.name = key;
|
|
select.id = `ai-tool-setting-${key}`;
|
|
for (const option of field.options || []) {
|
|
const item = document.createElement("option");
|
|
item.value = option;
|
|
item.textContent = option;
|
|
item.selected = option === value;
|
|
select.append(item);
|
|
}
|
|
return select;
|
|
}
|
|
const input = document.createElement("input");
|
|
input.name = key;
|
|
input.id = `ai-tool-setting-${key}`;
|
|
input.type = field.secret ? "password" : ["integer", "number"].includes(field.type) ? "number" : "text";
|
|
if (field.minimum != null) input.min = field.minimum;
|
|
if (field.maximum != null) input.max = field.maximum;
|
|
if (field.type === "number") input.step = "any";
|
|
input.value = field.secret ? "" : value ?? "";
|
|
if (field.secret && configuredSecret) input.placeholder = "Configured; leave blank to keep";
|
|
return input;
|
|
};
|
|
|
|
settingsForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
if (!activeSettingsTool) return;
|
|
const values = {};
|
|
for (const [key, field] of Object.entries(activeSettingsTool.settings_schema || {})) {
|
|
if (field.type === "multi_select") {
|
|
const group = [...settingsFields.querySelectorAll("[data-setting-name]")]
|
|
.find((element) => element.dataset.settingName === key);
|
|
values[key] = [...(group?.querySelectorAll("input:checked") || [])].map((input) => input.value);
|
|
} else {
|
|
const input = [...settingsFields.querySelectorAll("[name]")]
|
|
.find((element) => element.name === key);
|
|
values[key] = field.type === "boolean" ? Boolean(input?.checked) : input?.value;
|
|
}
|
|
}
|
|
settingsSave.disabled = true;
|
|
settingsSave.textContent = "Saving...";
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_ai/api/tools/${encodeURIComponent(activeSettingsTool.tool_id)}/settings`, {
|
|
method: "POST",
|
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
body: JSON.stringify({ values })
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Unable to save tool settings.");
|
|
setOpen(settingsModal, false);
|
|
await loadTools(false);
|
|
} catch (error) {
|
|
window.alert(error.message);
|
|
} finally {
|
|
settingsSave.disabled = false;
|
|
settingsSave.textContent = "Save settings";
|
|
}
|
|
});
|
|
|
|
settingsReset.addEventListener("click", async () => {
|
|
if (!activeSettingsTool) return;
|
|
if (!window.LumiConfirm?.destructiveFetch) {
|
|
window.alert("Timed confirmation is unavailable. Reload the page and try again.");
|
|
return;
|
|
}
|
|
settingsReset.disabled = true;
|
|
try {
|
|
const action = `/plugins/lumi_ai/api/tools/${encodeURIComponent(activeSettingsTool.tool_id)}/settings/reset`;
|
|
const response = await window.LumiConfirm.destructiveFetch(action, {
|
|
method: "POST",
|
|
headers: { Accept: "application/json" }
|
|
}, {
|
|
title: "Reset AI tool settings",
|
|
text: `Reset ${activeSettingsTool.display_name || activeSettingsTool.tool_id} settings to defaults?`,
|
|
label: "Reset settings"
|
|
});
|
|
if (!response) return;
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.error || "Unable to reset tool settings.");
|
|
renderSettings(payload);
|
|
await loadTools(false);
|
|
} catch (error) {
|
|
window.alert(error.message);
|
|
} finally {
|
|
settingsReset.disabled = false;
|
|
}
|
|
});
|
|
|
|
const renderMarkdown = (container, markdown) => {
|
|
container.replaceChildren();
|
|
const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n");
|
|
let paragraph = [];
|
|
const flush = () => {
|
|
if (!paragraph.length) return;
|
|
const p = document.createElement("p");
|
|
appendInline(p, paragraph.join(" "));
|
|
container.append(p);
|
|
paragraph = [];
|
|
};
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const fence = lines[index].match(/^```([a-z0-9_+-]*)/i);
|
|
if (fence) {
|
|
flush();
|
|
const code = [];
|
|
for (index += 1; index < lines.length && !/^```/.test(lines[index]); index += 1) code.push(lines[index]);
|
|
const pre = document.createElement("pre");
|
|
const element = document.createElement("code");
|
|
element.textContent = code.join("\n");
|
|
element.className = `language-${fence[1] || "text"}`;
|
|
pre.append(element);
|
|
container.append(pre);
|
|
continue;
|
|
}
|
|
const heading = lines[index].match(/^(#{1,4})\s+(.+)/);
|
|
if (heading) {
|
|
flush();
|
|
const element = document.createElement(`h${heading[1].length}`);
|
|
appendInline(element, heading[2]);
|
|
container.append(element);
|
|
} else if (/^\s*[-*]\s+/.test(lines[index])) {
|
|
flush();
|
|
const list = container.lastElementChild?.tagName === "UL" ? container.lastElementChild : document.createElement("ul");
|
|
const item = document.createElement("li");
|
|
appendInline(item, lines[index].replace(/^\s*[-*]\s+/, ""));
|
|
list.append(item);
|
|
if (!list.isConnected) container.append(list);
|
|
} else if (!lines[index].trim()) {
|
|
flush();
|
|
} else {
|
|
paragraph.push(lines[index]);
|
|
}
|
|
}
|
|
flush();
|
|
};
|
|
|
|
const appendInline = (parent, value) => {
|
|
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g;
|
|
let offset = 0;
|
|
for (const match of String(value).matchAll(pattern)) {
|
|
parent.append(document.createTextNode(value.slice(offset, match.index)));
|
|
const token = match[0];
|
|
if (token.startsWith("`")) {
|
|
const code = document.createElement("code");
|
|
code.textContent = token.slice(1, -1);
|
|
parent.append(code);
|
|
} else if (token.startsWith("**")) {
|
|
const strong = document.createElement("strong");
|
|
strong.textContent = token.slice(2, -2);
|
|
parent.append(strong);
|
|
} else {
|
|
const parts = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
const anchor = safeAnchor(parts?.[2], parts?.[1]);
|
|
parent.append(anchor || document.createTextNode(parts?.[1] || token));
|
|
}
|
|
offset = match.index + token.length;
|
|
}
|
|
parent.append(document.createTextNode(String(value).slice(offset)));
|
|
};
|
|
|
|
const safeAnchor = (href, label) => {
|
|
try {
|
|
const parsed = new URL(href, window.location.origin);
|
|
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
const anchor = document.createElement("a");
|
|
anchor.href = parsed.href;
|
|
anchor.textContent = label;
|
|
if (parsed.origin !== window.location.origin) {
|
|
anchor.target = "_blank";
|
|
anchor.rel = "noopener noreferrer";
|
|
}
|
|
return anchor;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const detail = (label, value) => {
|
|
const item = document.createElement("div");
|
|
const term = document.createElement("strong");
|
|
const body = document.createElement("span");
|
|
term.textContent = label;
|
|
body.textContent = value || "-";
|
|
item.append(term, body);
|
|
return item;
|
|
};
|
|
const dependencyStatus = (value) => {
|
|
const blocking = Array.isArray(value?.blocking) ? value.blocking : [];
|
|
const optional = Array.isArray(value?.optional) ? value.optional : [];
|
|
if (!blocking.length && !optional.length) return "Available";
|
|
return [
|
|
blocking.length ? `Blocking: ${blocking.join("; ")}` : "",
|
|
optional.length ? `Optional: ${optional.join("; ")}` : ""
|
|
].filter(Boolean).join(" | ");
|
|
};
|
|
const joinValue = (value) => Array.isArray(value) ? value.join(", ") || "-" : value && typeof value === "object" ? JSON.stringify(value) : String(value || "-");
|
|
const badge = (text, className = "") => {
|
|
const element = document.createElement("span");
|
|
element.className = `ai-tag ${className}`.trim();
|
|
element.textContent = text;
|
|
return element;
|
|
};
|
|
const textPair = (label, value) => {
|
|
const element = document.createElement("span");
|
|
element.className = "ai-tool-version";
|
|
element.textContent = `${label} ${value}`;
|
|
return element;
|
|
};
|
|
const button = (text, className = "") => {
|
|
const element = document.createElement("button");
|
|
element.type = "button";
|
|
element.className = `button ${className}`.trim();
|
|
element.textContent = text;
|
|
return element;
|
|
};
|
|
const message = (text, error = false) => {
|
|
const element = document.createElement("div");
|
|
element.className = `callout${error ? " danger" : ""}`;
|
|
element.textContent = text;
|
|
return element;
|
|
};
|
|
|
|
openButton.addEventListener("click", () => {
|
|
setOpen(modal, true);
|
|
loadTools(false);
|
|
});
|
|
refresh?.addEventListener("click", () => loadTools(true));
|
|
diagnosticRefresh?.addEventListener("click", loadDiagnostics);
|
|
diagnostics.addEventListener("toggle", () => {
|
|
if (diagnostics.open) loadDiagnostics();
|
|
});
|
|
diagnosticRole.addEventListener("change", () => diagnostics.open && loadDiagnostics());
|
|
diagnosticOrigin.addEventListener("change", () => diagnostics.open && loadDiagnostics());
|
|
modal.querySelectorAll("[data-ai-tools-close]").forEach((control) => control.addEventListener("click", () => setOpen(modal, false)));
|
|
readmeModal.querySelectorAll("[data-ai-tool-readme-close]").forEach((control) => control.addEventListener("click", () => setOpen(readmeModal, false)));
|
|
settingsModal.querySelectorAll("[data-ai-tool-settings-close]").forEach((control) => control.addEventListener("click", () => setOpen(settingsModal, false)));
|
|
for (const target of [modal, readmeModal, settingsModal]) {
|
|
target.addEventListener("click", (event) => {
|
|
if (event.target === target) setOpen(target, false);
|
|
});
|
|
}
|
|
})();
|