mirror of
https://github.com/we-promise/sure.git
synced 2026-09-02 05:11:05 +00:00
* Add live AI checks to system health Give super admins a dedicated AI status view with bounded liveness probes for LLMs, vector stores, pgvector, and embedding endpoints. Record sanitized failures in both the system debug log and Rails logger, and document the recommended local configuration.\n\nCloses #3145 * Fix AI health CI checks * Address AI health review feedback * Correct Ollama model preload guidance * Distinguish OpenAI-compatible providers * Make Ollama startup readiness explicit * Recognize Cloudflare AI endpoints
110 lines
3.2 KiB
JavaScript
110 lines
3.2 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
// Connects to data-controller="tabs--components"
|
|
export default class extends Controller {
|
|
static classes = ["navBtnActive", "navBtnInactive"];
|
|
static targets = ["panel", "navBtn"];
|
|
static values = {
|
|
sessionKey: String,
|
|
urlParamKey: String,
|
|
navigateOnChange: Boolean,
|
|
};
|
|
|
|
show(e) {
|
|
const btn = e.target.closest("button");
|
|
const selectedTabId = btn.dataset.id;
|
|
|
|
this.navBtnTargets.forEach((navBtn) => {
|
|
const isSelected = navBtn.dataset.id === selectedTabId;
|
|
if (isSelected) {
|
|
navBtn.classList.add(...this.navBtnActiveClasses);
|
|
navBtn.classList.remove(...this.navBtnInactiveClasses);
|
|
} else {
|
|
navBtn.classList.add(...this.navBtnInactiveClasses);
|
|
navBtn.classList.remove(...this.navBtnActiveClasses);
|
|
}
|
|
// Roving tabindex per WAI-ARIA APG: only the active tab is in
|
|
// the tab order. ArrowLeft/Right (see handleKeydown) moves focus
|
|
// across the tablist; Tab moves past the widget.
|
|
navBtn.setAttribute("aria-selected", isSelected.toString());
|
|
navBtn.setAttribute("tabindex", isSelected ? "0" : "-1");
|
|
});
|
|
|
|
this.panelTargets.forEach((panel) => {
|
|
if (panel.dataset.id === selectedTabId) {
|
|
panel.classList.remove("hidden");
|
|
} else {
|
|
panel.classList.add("hidden");
|
|
}
|
|
});
|
|
|
|
if (this.urlParamKeyValue) {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set(this.urlParamKeyValue, selectedTabId);
|
|
|
|
if (this.navigateOnChangeValue) {
|
|
window.location.assign(url.toString());
|
|
return;
|
|
}
|
|
|
|
window.history.replaceState({}, "", url);
|
|
}
|
|
|
|
// Update URL with the selected tab
|
|
if (this.sessionKeyValue) {
|
|
this.#updateSessionPreference(selectedTabId);
|
|
}
|
|
}
|
|
|
|
// WAI-ARIA APG "Tabs with Manual Activation" — arrow keys move
|
|
// focus, Enter/Space activates. Prevents accidental tab swap when
|
|
// tabbing through, which is important here because some tab
|
|
// contents trigger Turbo fetches.
|
|
handleKeydown(e) {
|
|
const navBtns = this.navBtnTargets;
|
|
const currentIndex = navBtns.indexOf(e.target);
|
|
if (currentIndex === -1) return;
|
|
|
|
let nextIndex = null;
|
|
switch (e.key) {
|
|
case "ArrowRight":
|
|
nextIndex = (currentIndex + 1) % navBtns.length;
|
|
break;
|
|
case "ArrowLeft":
|
|
nextIndex = (currentIndex - 1 + navBtns.length) % navBtns.length;
|
|
break;
|
|
case "Home":
|
|
nextIndex = 0;
|
|
break;
|
|
case "End":
|
|
nextIndex = navBtns.length - 1;
|
|
break;
|
|
case "Enter":
|
|
case " ":
|
|
e.preventDefault();
|
|
this.show(e);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
navBtns[nextIndex].focus();
|
|
}
|
|
|
|
#updateSessionPreference(selectedTabId) {
|
|
fetch("/current_session", {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"X-CSRF-Token": document.querySelector('[name="csrf-token"]').content,
|
|
Accept: "application/json",
|
|
},
|
|
body: new URLSearchParams({
|
|
"current_session[tab_key]": this.sessionKeyValue,
|
|
"current_session[tab_value]": selectedTabId,
|
|
}).toString(),
|
|
});
|
|
}
|
|
}
|