dheraingoud's picture
feat: sync upstream commits up to f17c92bc
a1bab2d
Raw
History Blame Contribute Delete
31.3 kB
const state = {
config: null,
fields: new Map(),
localStatus: new Map(),
modelOptions: [],
modelComboboxes: new Set(),
authPollers: new Map(),
activeView: "providers",
};
const MASKED_SECRET = "********";
const VIEW_GROUPS = [
{
id: "providers",
label: "Providers",
title: "Providers",
sections: ["providers", "runtime"],
containerId: "providersSections",
},
{
id: "model_config",
label: "Model Config",
title: "Model Config",
sections: ["models", "reasoning", "web_tools"],
containerId: "modelConfigSections",
},
{
id: "messaging",
label: "Messaging",
title: "Messaging",
sections: ["messaging", "voice"],
containerId: "messagingSections",
},
];
const byId = (id) => document.getElementById(id);
function sourceLabel(source) {
const labels = {
default: "default",
template: "template",
repo_env: "repo .env",
managed_env: "",
explicit_env_file: "FCC_ENV_FILE",
process: "process env",
};
return Object.prototype.hasOwnProperty.call(labels, source) ? labels[source] : source;
}
function sourceText(field) {
const parts = [];
const label = sourceLabel(field.source);
if (label) {
parts.push(label);
}
if (field.locked) {
parts.push("locked");
}
return parts.join(" ");
}
function statusClass(status) {
if (["configured", "reachable", "running", "connected"].includes(status)) return "ok";
if (["missing_key", "missing_config", "missing_url", "unknown", "connecting"].includes(status)) return "warn";
if (["offline", "error"].includes(status)) return "error";
return "neutral";
}
async function api(path, options = {}) {
const response = await fetch(path, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
cache: "no-store",
});
if (!response.ok) {
let detail = "";
try {
const payload = await response.json();
detail = typeof payload.detail === "string" ? payload.detail : "";
} catch {
// The status remains useful when an upstream proxy returns a non-JSON page.
}
throw new Error(detail || `${response.status} ${response.statusText}`);
}
return response.json();
}
async function load() {
showMessage("Loading admin config");
const config = await api("/admin/api/config");
state.config = config;
state.fields = new Map(config.fields.map((field) => [field.key, field]));
renderNav();
renderProviders(config.provider_status);
renderSections(config.sections, config.fields);
byId("configPath").textContent = config.paths.managed;
await refreshConnectedAccounts();
await hydrateModelOptions();
await validate(false);
await refreshLocalStatus();
updateDirtyState();
showMessage("");
}
function renderNav() {
const nav = byId("sectionNav");
nav.innerHTML = "";
VIEW_GROUPS.forEach((view, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = `nav-link${index === 0 ? " active" : ""}`;
button.dataset.view = view.id;
button.textContent = view.label;
if (index === 0) {
button.setAttribute("aria-current", "page");
}
button.addEventListener("click", () => {
setActiveView(view.id, { scroll: true });
});
nav.appendChild(button);
});
setActiveView(state.activeView, { scroll: false });
}
function setActiveView(viewId, { scroll = false } = {}) {
const activeView =
VIEW_GROUPS.find((view) => view.id === viewId) || VIEW_GROUPS[0];
state.activeView = activeView.id;
byId("pageTitle").textContent = activeView.title;
document.querySelectorAll(".nav-link").forEach((link) => {
const selected = link.dataset.view === activeView.id;
link.classList.toggle("active", selected);
if (selected) {
link.setAttribute("aria-current", "page");
} else {
link.removeAttribute("aria-current");
}
});
document.querySelectorAll(".admin-view").forEach((view) => {
const selected = view.dataset.view === activeView.id;
view.classList.toggle("active", selected);
view.hidden = !selected;
});
if (scroll) {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
function renderProviders(providerStatus) {
const grid = byId("providerGrid");
const connectedGrid = byId("connectedAccountGrid");
grid.innerHTML = "";
connectedGrid.innerHTML = "";
const connected = providerStatus.filter(
(provider) => provider.kind === "connected_account",
);
byId("connectedAccountsSection").hidden = connected.length === 0;
providerStatus.forEach((provider) => {
if (provider.kind === "connected_account") {
connectedGrid.appendChild(renderConnectedAccountCard(provider));
return;
}
const card = document.createElement("article");
card.className = "provider-card";
card.dataset.provider = provider.provider_id;
const title = document.createElement("div");
title.className = "provider-title";
title.innerHTML = `<strong>${provider.display_name || provider.provider_id}</strong>`;
const pill = document.createElement("span");
pill.className = `status-pill ${statusClass(provider.status)}`;
pill.textContent = provider.label;
title.appendChild(pill);
const meta = document.createElement("div");
meta.className = "provider-meta";
meta.textContent =
provider.kind === "local"
? provider.base_url || "No local URL configured"
: provider.configuration;
const button = document.createElement("button");
button.type = "button";
button.className = "test-button";
button.textContent = provider.kind === "local" ? "Test" : "Refresh models";
button.addEventListener("click", () => testProvider(provider.provider_id, button));
card.append(title, meta, button);
grid.appendChild(card);
});
}
function renderConnectedAccountCard(provider, status = provider) {
const card = document.createElement("article");
card.className = "provider-card";
card.dataset.provider = provider.provider_id;
card.dataset.connectedAccount = "true";
const title = document.createElement("div");
title.className = "provider-title";
const name = document.createElement("strong");
name.textContent = provider.display_name || provider.provider_id;
const pill = document.createElement("span");
pill.className = `status-pill ${statusClass(status.state || status.status)}`;
pill.textContent = connectedAccountLabel(status);
title.append(name, pill);
const meta = document.createElement("div");
meta.className = "provider-meta";
meta.textContent = connectedAccountMeta(status);
const actions = document.createElement("div");
actions.className = "provider-actions";
populateConnectedAccountActions(provider, status, actions);
card.append(title, meta, actions);
return card;
}
function connectedAccountLabel(status) {
const labels = {
disconnected: "Not connected",
connecting: "Connecting",
connected: "Connected",
error: "Needs attention",
};
return labels[status.state] || status.label || "Not connected";
}
function connectedAccountMeta(status) {
if (status.connected) {
const identity = status.email || "ChatGPT subscription connected";
const models = Number.isInteger(status.model_count)
? `${status.model_count} model${status.model_count === 1 ? "" : "s"} available. `
: "";
const error = status.message ? `${status.message} ` : "";
return `${identity}. ${models}${error}Restart your agent to refresh its model picker.`;
}
if (status.mode === "device" && status.user_code) {
return `Enter code ${status.user_code} at ${status.verification_url}`;
}
if (status.state === "connecting") {
return "Finish signing in, then return to this page.";
}
return status.message || "Connect a ChatGPT account to discover subscription models.";
}
function populateConnectedAccountActions(provider, status, actions) {
const providerId = provider.provider_id;
if (status.state === "connecting") {
const target = status.authorization_url || status.verification_url;
if (target) {
actions.appendChild(authButton("Open sign-in", () => window.open(target, "_blank", "noopener")));
}
if (status.mode === "device" && status.user_code) {
actions.appendChild(
authButton(
"Copy code",
() => copyDeviceCode(status.user_code),
"secondary-button",
),
);
}
actions.appendChild(
authButton("Cancel", () => cancelConnectedAccountLogin(providerId), "secondary-button"),
);
return;
}
if (status.connected) {
actions.appendChild(
authButton(
"Reconnect",
(button) => startConnectedAccountLogin(providerId, "browser", button),
),
);
actions.appendChild(
authButton(
"Disconnect",
() => disconnectConnectedAccount(providerId),
"secondary-button",
),
);
return;
}
actions.appendChild(
authButton("Connect", (button) => startConnectedAccountLogin(providerId, "browser", button)),
authButton(
"Use device code",
(button) => startConnectedAccountLogin(providerId, "device", button),
"secondary-button",
),
);
}
function authButton(label, action, className = "test-button") {
const button = document.createElement("button");
button.type = "button";
button.className = className;
button.textContent = label;
button.addEventListener("click", () => action(button));
return button;
}
async function refreshConnectedAccounts() {
const providers = (state.config?.provider_status || []).filter(
(provider) => provider.kind === "connected_account",
);
await Promise.all(
providers.map(async (provider) => {
try {
const status = await api(`/admin/api/providers/${provider.provider_id}/auth`);
updateConnectedAccountCard(provider, status);
if (status.state === "connecting") pollConnectedAccount(provider);
} catch (error) {
updateConnectedAccountCard(provider, {
state: "error",
connected: false,
message: error.message,
});
}
}),
);
}
function updateConnectedAccountCard(provider, status) {
const current = document.querySelector(
`[data-provider="${provider.provider_id}"][data-connected-account="true"]`,
);
if (current) current.replaceWith(renderConnectedAccountCard(provider, status));
}
async function startConnectedAccountLogin(providerId, mode, button) {
button.disabled = true;
const popup = window.open("about:blank", "_blank");
if (popup) popup.opener = null;
try {
const status = await api(`/admin/api/providers/${providerId}/auth/login`, {
method: "POST",
body: JSON.stringify({ mode }),
});
const provider = connectedAccountDescriptor(providerId);
updateConnectedAccountCard(provider, status);
const target = status.authorization_url || status.verification_url;
if (target && popup) {
popup.location.replace(target);
} else if (target) {
window.open(target, "_blank", "noopener");
} else if (popup) {
popup.close();
}
pollConnectedAccount(provider);
} catch (error) {
if (popup) popup.close();
showMessage(error.message, true);
button.disabled = false;
}
}
async function cancelConnectedAccountLogin(providerId) {
clearConnectedAccountPoll(providerId);
const status = await api(`/admin/api/providers/${providerId}/auth/cancel`, {
method: "POST",
});
updateConnectedAccountCard(connectedAccountDescriptor(providerId), status);
}
async function disconnectConnectedAccount(providerId) {
if (!window.confirm("Disconnect this ChatGPT account from FCC?")) return;
clearConnectedAccountPoll(providerId);
const status = await api(`/admin/api/providers/${providerId}/auth`, {
method: "DELETE",
});
updateConnectedAccountCard(connectedAccountDescriptor(providerId), status);
await hydrateModelOptions();
}
function pollConnectedAccount(provider) {
clearConnectedAccountPoll(provider.provider_id);
const poll = async () => {
try {
const status = await api(`/admin/api/providers/${provider.provider_id}/auth`);
updateConnectedAccountCard(provider, status);
if (status.state === "connecting") {
state.authPollers.set(provider.provider_id, window.setTimeout(poll, 1000));
} else {
state.authPollers.delete(provider.provider_id);
if (status.connected) await hydrateModelOptions();
}
} catch (error) {
state.authPollers.delete(provider.provider_id);
showMessage(error.message, true);
}
};
state.authPollers.set(provider.provider_id, window.setTimeout(poll, 1000));
}
function clearConnectedAccountPoll(providerId) {
const timer = state.authPollers.get(providerId);
if (timer) window.clearTimeout(timer);
state.authPollers.delete(providerId);
}
function connectedAccountDescriptor(providerId) {
return state.config.provider_status.find(
(provider) => provider.provider_id === providerId,
);
}
async function copyDeviceCode(code) {
try {
await navigator.clipboard.writeText(code);
showMessage("Device code copied.");
} catch {
showMessage(`Copy this device code: ${code}`);
}
}
function updateProviderCard(providerId, status, label, metaText) {
const card = document.querySelector(`[data-provider="${providerId}"]`);
if (!card) return;
const pill = card.querySelector(".status-pill");
pill.className = `status-pill ${statusClass(status)}`;
pill.textContent = label;
if (metaText) {
card.querySelector(".provider-meta").textContent = metaText;
}
}
function renderSections(sections, fields) {
state.modelComboboxes.clear();
VIEW_GROUPS.forEach((view) => {
byId(view.containerId).innerHTML = "";
});
const sectionById = new Map(sections.map((section) => [section.id, section]));
const bySection = new Map();
sections.forEach((section) => bySection.set(section.id, []));
fields.forEach((field) => {
if (!bySection.has(field.section)) bySection.set(field.section, []);
bySection.get(field.section).push(field);
});
VIEW_GROUPS.forEach((view) => {
const container = byId(view.containerId);
view.sections.forEach((sectionId) => {
const section = sectionById.get(sectionId);
const sectionFields = bySection.get(sectionId) || [];
if (!section || sectionFields.length === 0) return;
const sectionEl = document.createElement("section");
sectionEl.className = "settings-section";
sectionEl.id = `section-${section.id}`;
const heading = document.createElement("div");
heading.className = "section-heading";
heading.innerHTML = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
if (section.id === "models") {
const refreshButton = document.createElement("button");
refreshButton.type = "button";
refreshButton.className = "secondary-button";
refreshButton.textContent = "Refresh models";
refreshButton.addEventListener("click", () => refreshModelOptions(refreshButton));
heading.appendChild(refreshButton);
}
sectionEl.appendChild(heading);
const grid = document.createElement("div");
grid.className = "field-grid";
sectionFields.forEach((field) => {
grid.appendChild(renderField(field));
});
sectionEl.appendChild(grid);
if (sectionFields.some((field) => field.advanced)) {
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "ghost-button advanced-toggle";
toggle.textContent = "Show advanced";
toggle.addEventListener("click", () => {
const showing = sectionEl.classList.toggle("show-advanced");
toggle.textContent = showing ? "Hide advanced" : "Show advanced";
});
sectionEl.appendChild(toggle);
}
container.appendChild(sectionEl);
});
});
}
function renderField(field) {
const wrapper = document.createElement("div");
wrapper.className = `field${field.advanced ? " advanced-field" : ""}`;
wrapper.dataset.key = field.key;
const label = document.createElement("label");
label.htmlFor = `field-${field.key}`;
const labelText = document.createElement("span");
labelText.textContent = field.label;
label.appendChild(labelText);
const source = sourceText(field);
if (source) {
const sourceEl = document.createElement("span");
sourceEl.className = "field-source";
sourceEl.textContent = source;
label.appendChild(sourceEl);
}
const input = inputForField(field);
input.id = `field-${field.key}`;
input.dataset.key = field.key;
input.dataset.original = field.value || "";
input.dataset.secret = field.secret ? "true" : "false";
input.dataset.configured = field.configured ? "true" : "false";
input.dataset.fieldType = field.type;
input.disabled = field.locked;
input.addEventListener("input", updateDirtyState);
input.addEventListener("change", updateDirtyState);
if (field.type === "optional_model") {
input.addEventListener("blur", () => {
if (!input.value.trim() || input.value.trim().toLowerCase() === "none") {
input.value = "None";
updateDirtyState();
}
});
}
const control =
field.type === "model" || field.type === "optional_model"
? new ModelCombobox(input, field).element
: input;
wrapper.append(label, control);
if (field.description) {
const description = document.createElement("div");
description.className = "field-description";
description.textContent = field.description;
wrapper.appendChild(description);
}
return wrapper;
}
function inputForField(field) {
if (field.type === "boolean") {
const input = document.createElement("input");
input.type = "checkbox";
input.checked = String(field.value).toLowerCase() === "true";
input.dataset.original = input.checked ? "true" : "false";
return input;
}
if (field.type === "select") {
const select = document.createElement("select");
field.options.forEach((item) =>
select.appendChild(option(item.value, item.label)),
);
select.value = field.value || field.options[0]?.value || "";
return select;
}
if (field.type === "textarea") {
const textarea = document.createElement("textarea");
textarea.value = field.value || "";
return textarea;
}
if (field.type === "model" || field.type === "optional_model") {
const input = document.createElement("input");
input.type = "text";
input.value = field.value || (field.type === "optional_model" ? "None" : "");
input.autocomplete = "off";
return input;
}
const input = document.createElement("input");
input.type = field.type === "number" ? "number" : "text";
if (field.type === "secret") {
input.type = "password";
input.placeholder = field.configured
? "Configured - enter a new value to replace"
: "Not configured";
input.value = "";
input.autocomplete = "off";
} else {
input.value = field.value || "";
}
return input;
}
class ModelCombobox {
constructor(input, field) {
this.input = input;
this.fieldType = field.type;
this.activeIndex = -1;
this.query = "";
this.element = document.createElement("div");
this.element.className = "model-combobox";
this.listbox = document.createElement("div");
this.listbox.className = "model-combobox-list";
this.listbox.id = `model-options-${field.key}`;
this.listbox.setAttribute("role", "listbox");
this.listbox.hidden = true;
this.toggle = document.createElement("button");
this.toggle.type = "button";
this.toggle.className = "model-combobox-toggle";
this.toggle.disabled = input.disabled;
this.toggle.setAttribute("aria-label", `Show ${field.label} options`);
input.setAttribute("role", "combobox");
input.setAttribute("aria-autocomplete", "list");
input.setAttribute("aria-haspopup", "listbox");
for (const control of [input, this.toggle]) {
control.setAttribute("aria-controls", this.listbox.id);
control.setAttribute("aria-expanded", "false");
}
input.addEventListener("click", () => this.open());
input.addEventListener("input", () => this.open(input.value));
input.addEventListener("keydown", (event) => this.handleKeydown(event));
this.toggle.addEventListener("mousedown", (event) => event.preventDefault());
this.toggle.addEventListener("click", () => {
if (this.isOpen) this.close();
else this.open();
input.focus();
});
this.listbox.addEventListener("mousedown", (event) => event.preventDefault());
this.listbox.addEventListener("mousemove", (event) => {
const optionEl = event.target.closest('[role="option"]');
if (optionEl) this.setActive(this.visibleOptions.indexOf(optionEl));
});
this.listbox.addEventListener("click", (event) => {
const optionEl = event.target.closest('[role="option"]');
if (optionEl) this.select(optionEl.dataset.value);
});
this.element.append(input, this.toggle, this.listbox);
state.modelComboboxes.add(this);
}
get isOpen() {
return this.element.classList.contains("open");
}
get values() {
return this.fieldType === "optional_model"
? ["None", ...state.modelOptions]
: state.modelOptions;
}
get visibleOptions() {
return Array.from(this.listbox.querySelectorAll('[role="option"]'));
}
open(query = "") {
if (this.input.disabled) return;
state.modelComboboxes.forEach((combobox) => {
if (combobox !== this) combobox.close();
});
this.render(query);
this.element.classList.add("open");
this.listbox.hidden = false;
this.setExpanded(true);
}
close() {
this.element.classList.remove("open");
this.listbox.hidden = true;
this.activeIndex = -1;
this.input.removeAttribute("aria-activedescendant");
this.setExpanded(false);
}
setExpanded(expanded) {
for (const control of [this.input, this.toggle]) {
control.setAttribute("aria-expanded", String(expanded));
}
}
render(query) {
this.query = query;
const normalizedQuery = query.trim().toLocaleLowerCase();
const values = normalizedQuery
? this.values.filter((value) =>
value.toLocaleLowerCase().includes(normalizedQuery),
)
: this.values;
this.listbox.innerHTML = "";
if (values.length === 0) {
const empty = document.createElement("div");
empty.className = "model-combobox-empty";
empty.textContent = state.modelOptions.length
? "No matching models. You can still enter a custom slug."
: "No discovered models. Refresh models or enter a custom slug.";
this.listbox.appendChild(empty);
this.activeIndex = -1;
this.input.removeAttribute("aria-activedescendant");
return;
}
values.forEach((value, index) => {
const optionEl = document.createElement("div");
optionEl.className = "model-combobox-option";
optionEl.id = `${this.listbox.id}-option-${index}`;
optionEl.dataset.value = value;
optionEl.setAttribute("role", "option");
optionEl.textContent = value;
this.listbox.appendChild(optionEl);
});
const selectedIndex = values.indexOf(this.input.value);
this.setActive(selectedIndex >= 0 ? selectedIndex : 0, false);
}
setActive(index, scroll = true) {
const options = this.visibleOptions;
if (options.length === 0) return;
this.activeIndex = Math.max(0, Math.min(index, options.length - 1));
options.forEach((optionEl, optionIndex) => {
const active = optionIndex === this.activeIndex;
optionEl.classList.toggle("active", active);
optionEl.setAttribute("aria-selected", String(active));
});
const activeOption = options[this.activeIndex];
this.input.setAttribute("aria-activedescendant", activeOption.id);
if (scroll) activeOption.scrollIntoView({ block: "nearest" });
}
move(offset) {
const count = this.visibleOptions.length;
if (count) this.setActive((this.activeIndex + offset + count) % count);
}
select(value) {
this.input.value = value;
this.input.dispatchEvent(new Event("change", { bubbles: true }));
this.close();
this.input.focus();
}
handleKeydown(event) {
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
if (this.isOpen) {
this.move(event.key === "ArrowDown" ? 1 : -1);
} else {
this.open();
if (event.key === "ArrowUp") {
this.setActive(this.visibleOptions.length - 1);
}
}
} else if (this.isOpen && (event.key === "Home" || event.key === "End")) {
event.preventDefault();
this.setActive(event.key === "Home" ? 0 : this.visibleOptions.length - 1);
} else if (this.isOpen && event.key === "Enter") {
const active = this.visibleOptions[this.activeIndex];
if (active) {
event.preventDefault();
this.select(active.dataset.value);
}
} else if (this.isOpen && event.key === "Escape") {
event.preventDefault();
this.close();
} else if (this.isOpen && event.key === "Tab") {
this.close();
}
}
}
function option(value, label) {
const optionEl = document.createElement("option");
optionEl.value = value;
optionEl.textContent = label;
return optionEl;
}
function readFieldValue(input) {
if (input.type === "checkbox") return input.checked ? "true" : "false";
if (
input.dataset.fieldType === "optional_model" &&
input.value.trim().toLowerCase() === "none"
) {
return "";
}
if (input.dataset.secret === "true" && input.dataset.configured === "true") {
return input.value ? input.value : MASKED_SECRET;
}
return input.value;
}
function changedValues() {
const values = {};
document.querySelectorAll("[data-key]").forEach((input) => {
if (input.disabled || !input.matches("input, select, textarea")) return;
const value = readFieldValue(input);
if (value !== input.dataset.original) {
values[input.dataset.key] = value;
}
});
return values;
}
function updateDirtyState() {
const count = Object.keys(changedValues()).length;
byId("dirtyState").textContent =
count === 0 ? "No changes" : `${count} unsaved change${count === 1 ? "" : "s"}`;
byId("applyButton").disabled = count === 0;
}
async function validate(showResult = true) {
const result = await api("/admin/api/config/validate", {
method: "POST",
body: JSON.stringify({ values: changedValues() }),
});
if (showResult) {
showValidationResult(result);
}
return result;
}
function showValidationResult(result) {
if (result.valid) {
showMessage("Config shape is valid", "ok");
} else {
showMessage(result.errors.join("; "), "error");
}
}
async function apply() {
const result = await api("/admin/api/config/apply", {
method: "POST",
body: JSON.stringify({ values: changedValues() }),
});
if (!result.applied) {
showValidationResult(result);
return;
}
const restart = result.restart || {};
if (restart.required && restart.automatic) {
showMessage("Applied. Restarting server...", "ok");
byId("applyButton").disabled = true;
setTimeout(() => {
window.location.href = restart.admin_url || "/admin";
}, 1600);
return;
}
const pending = restart.required ? restart.fields || [] : result.pending_fields || [];
await load();
showMessage(
pending.length
? `Applied. Restart fcc-server to use: ${pending.join(", ")}`
: "Applied",
"ok",
);
}
async function refreshLocalStatus() {
const result = await api("/admin/api/providers/local-status");
result.providers.forEach((provider) => {
state.localStatus.set(provider.provider_id, provider);
const meta = provider.status_code
? `${provider.base_url} returned HTTP ${provider.status_code}`
: provider.base_url;
updateProviderCard(provider.provider_id, provider.status, provider.label, meta);
});
}
async function testProvider(providerId, button) {
const original = button.textContent;
button.disabled = true;
button.textContent = "Testing";
try {
const result = await api(`/admin/api/providers/${providerId}/test`, {
method: "POST",
body: "{}",
});
if (result.ok) {
updateProviderCard(
providerId,
"reachable",
`${result.models.length} models`,
result.models.slice(0, 3).join(", ") || "No models returned",
);
setModelOptions([
...state.modelOptions,
...result.models.map((model) => `${providerId}/${model}`),
]);
} else {
updateProviderCard(providerId, "offline", result.error_type, result.error_type);
}
} finally {
button.disabled = false;
button.textContent = original;
}
}
async function hydrateModelOptions() {
try {
await loadModelOptions();
} catch {
// Model fields remain editable when optional catalog hydration is unavailable.
}
}
async function loadModelOptions(refresh = false) {
const result = await api("/admin/api/models" + (refresh ? "/refresh" : ""), {
method: refresh ? "POST" : "GET",
});
setModelOptions(result.models);
return result;
}
async function refreshModelOptions(button) {
const original = button.textContent;
button.disabled = true;
button.textContent = "Refreshing";
try {
const result = await loadModelOptions(true);
const failedProviders = result.failed_providers || [];
if (failedProviders.length) {
const labels = failedProviders.map(providerDisplayName).join(", ");
showMessage(
`${state.modelOptions.length} models available; could not refresh ${labels}`,
"warn",
);
} else {
showMessage(`${state.modelOptions.length} models available`, "ok");
}
} catch (error) {
showMessage(`Could not refresh models: ${error.message}`, "error");
} finally {
button.disabled = false;
button.textContent = original;
}
}
function providerDisplayName(providerId) {
const provider = state.config?.provider_status?.find(
(candidate) => candidate.provider_id === providerId,
);
return provider?.display_name || providerId;
}
function setModelOptions(models) {
state.modelOptions = Array.from(
new Set(models.filter((model) => typeof model === "string" && model.trim())),
).sort((left, right) => left.localeCompare(right));
state.modelComboboxes.forEach((combobox) => {
if (combobox.isOpen) combobox.render(combobox.query);
});
}
function showMessage(message, kind = "") {
const area = byId("messageArea");
area.textContent = message;
area.className = `message-area ${kind}`.trim();
}
byId("validateButton").addEventListener("click", () => validate(true));
byId("applyButton").addEventListener("click", apply);
document.addEventListener("pointerdown", (event) => {
state.modelComboboxes.forEach((combobox) => {
if (combobox.isOpen && !combobox.element.contains(event.target)) combobox.close();
});
});
load().catch((error) => {
showMessage(error.message, "error");
});