import { el, setText, clear } from './dom.js';
import { getJson, ApiError, routes } from './api.js';
import * as chat from './chat.js';
import * as models from './models.js';
import * as activity from './activity.js';
import * as connect from './connect.js';
const SCREENS = { chat, models, activity, connect };
const DEFAULT_SCREEN = 'chat';
const main = document.getElementById('main');
const nav = document.getElementById('nav');
const pill = document.getElementById('health-pill');
const dot = document.getElementById('health-dot');
const pillText = document.getElementById('health-text');
const detail = document.getElementById('health-detail');
export const health = { current: null };
let teardown = null;
function screenFor(pathname) {
const parts = pathname.split('/').filter(Boolean);
const name = parts[0] === 'ui' ? parts[1] : parts[0];
return Object.prototype.hasOwnProperty.call(SCREENS, name) ? name : DEFAULT_SCREEN;
}
function render(pathname) {
const name = screenFor(pathname);
if (teardown) {
try { teardown(); } catch { }
teardown = null;
}
clear(main);
document.title = `Ferrox Studio — ${SCREENS[name].title}`;
for (const link of nav.querySelectorAll('a')) {
if (link.dataset.screen === name) link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
}
try {
teardown = SCREENS[name].mount(main) || null;
} catch (error) {
main.appendChild(
el('p', { class: 'notice notice-error', text: `This screen failed to render: ${error.message}` }),
);
}
}
function navigate(pathname, { replace = false } = {}) {
if (replace) window.history.replaceState({}, '', pathname);
else window.history.pushState({}, '', pathname);
render(pathname);
main.focus({ preventScroll: true });
}
document.addEventListener('click', (event) => {
if (event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const anchor = event.target.closest?.('a[href^="/"]');
if (!anchor || anchor.target === '_blank') return;
event.preventDefault();
navigate(anchor.getAttribute('href'));
});
window.addEventListener('popstate', () => render(window.location.pathname));
function paintHealth(body, error) {
if (error) {
dot.dataset.state = 'down';
setText(pillText, error.status === 503 ? 'unavailable' : 'unreachable');
return;
}
const state = body.state || 'unavailable';
dot.dataset.state = state;
const label =
state === 'ready'
? body.model?.id || 'ready'
: state === 'detecting'
? 'detecting backends…'
: body.reason || 'unavailable';
setText(pillText, label);
}
function paintHealthDetail() {
clear(detail);
const body = health.current;
if (!body) {
detail.appendChild(el('p', { class: 'muted', text: 'The server did not answer /health.' }));
return;
}
const head = [`state: ${body.state}`];
if (body.detail) head.push(body.detail);
if (body.model) {
head.push(
`model: ${body.model.id} (tokenizer ${body.model.tokenizer})${
body.model.synthetic_weights ? ' — SYNTHETIC random weights, output is noise' : ''
}`,
);
}
detail.appendChild(el('div', { text: head.join(' · ') }));
const list = el('ul');
for (const cap of body.capabilities || []) {
list.appendChild(
el('li', { class: cap.available ? 'cap-yes' : 'cap-no', title: cap.detail }, [
el('strong', { text: cap.id }),
` — ${cap.available ? 'available' : cap.reason}: ${cap.detail}`,
]),
);
}
detail.appendChild(list);
detail.appendChild(
el('p', { class: 'small faint', text: `version ${body.version} · pid ${body.pid}` }),
);
}
pill.addEventListener('click', () => {
const open = detail.hidden;
detail.hidden = !open;
pill.setAttribute('aria-expanded', String(open));
if (open) paintHealthDetail();
});
async function pollHealth() {
try {
health.current = await getJson(routes.health);
paintHealth(health.current, null);
} catch (error) {
if (error instanceof ApiError && error.body?.state) {
health.current = error.body;
paintHealth(error.body, null);
} else {
health.current = null;
paintHealth(null, error);
}
}
if (!detail.hidden) paintHealthDetail();
}
pollHealth();
setInterval(pollHealth, 5000);
const initial = window.location.pathname;
if (screenFor(initial) === DEFAULT_SCREEN && !initial.startsWith('/ui/')) {
navigate(`/ui/${DEFAULT_SCREEN}`, { replace: true });
} else {
render(initial);
}
export { navigate };