- OpenAI-compatible gateway with intelligent routing via cortiq-router (allaigate) - Embedded build-free admin SPA at /admin: 7 languages (en/ru/de/fr/es/zh/tr), light/dark themes, hot config reload (no restart) - Admin API, per-request statistics + Prometheus /metrics, secret store, playground - English documentation (primary) with Russian translations (*.ru.md)
138 lines
5.4 KiB
JavaScript
Vendored
138 lines
5.4 KiB
JavaScript
Vendored
// Playground — send a prompt through the live pipeline, inspect routing decision.
|
|
import { h, mount, money, ms, num } from "../ui.js";
|
|
import { t } from "../i18n.js";
|
|
import { api } from "../api.js";
|
|
import { appState } from "../app.js";
|
|
|
|
export async function renderPlayground() {
|
|
const meta = appState.meta || (await api.meta());
|
|
const modelsData = await api.listModels();
|
|
const ids = (modelsData.models || []).map((m) => m.id);
|
|
|
|
let mode = "auto";
|
|
const root = h("div");
|
|
|
|
const sysIn = h("textarea", { placeholder: t("pg.systemPlaceholder"), rows: 2 });
|
|
const promptIn = h("textarea", { placeholder: t("pg.promptPlaceholder"), rows: 5 });
|
|
const tempIn = h("input", { type: "number", step: "0.1", value: 0.7 });
|
|
const maxTokIn = h("input", { type: "number", value: 512 });
|
|
const profileSel = h("select", {}, ...(meta.profiles || []).map((p) => h("option", { value: p }, p)));
|
|
const modelSel = h("select", {}, ...ids.map((id) => h("option", { value: id }, id)));
|
|
|
|
const modeSeg = h(
|
|
"div",
|
|
{ class: "seg" },
|
|
h("button", { class: "active", onClick: (e) => setMode("auto", e) }, t("pg.mode.auto")),
|
|
h("button", { onClick: (e) => setMode("pinned", e) }, t("pg.mode.pinned"))
|
|
);
|
|
function buildModeExtra() {
|
|
return h(
|
|
"label",
|
|
{ class: "field" },
|
|
h("span", {}, mode === "auto" ? t("pg.profile") : t("pg.model")),
|
|
mode === "auto" ? profileSel : modelSel
|
|
);
|
|
}
|
|
let modeExtra = buildModeExtra();
|
|
function setMode(m, e) {
|
|
mode = m;
|
|
modeSeg.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
|
e.target.classList.add("active");
|
|
const next = buildModeExtra();
|
|
modeExtra.replaceWith(next);
|
|
modeExtra = next;
|
|
}
|
|
|
|
const out = h("div", { class: "answer" }, h("div", { class: "empty" }, t("pg.empty")));
|
|
const decision = h("div", { class: "card routecard" }, h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))), h("div", { class: "empty" }, "—"));
|
|
|
|
const sendBtn = h("button", { class: "btn primary" }, t("pg.send"));
|
|
sendBtn.addEventListener("click", async () => {
|
|
const prompt = promptIn.value.trim();
|
|
if (!prompt) return;
|
|
const messages = [];
|
|
if (sysIn.value.trim()) messages.push({ role: "system", content: sysIn.value.trim() });
|
|
messages.push({ role: "user", content: prompt });
|
|
let model = "cortiq-auto";
|
|
if (mode === "auto") {
|
|
if (profileSel.value) model = "cortiq-auto:" + profileSel.value;
|
|
} else {
|
|
model = modelSel.value;
|
|
}
|
|
sendBtn.disabled = true;
|
|
sendBtn.textContent = t("pg.sending");
|
|
mount(out, h("div", { class: "flex" }, h("span", { class: "spinner" }), " ", t("pg.sending")));
|
|
try {
|
|
const r = await api.test({
|
|
model,
|
|
messages,
|
|
temperature: parseFloat(tempIn.value),
|
|
max_tokens: parseInt(maxTokIn.value) || null,
|
|
});
|
|
if (r.ok) {
|
|
mount(out, r.answer || h("span", { class: "muted" }, "—"));
|
|
renderDecision(r);
|
|
} else {
|
|
mount(out, h("div", { class: "callout" }, h("b", {}, t("pg.error") + ": "), r.error || ""));
|
|
mount(decision, h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))), h("div", { class: "empty" }, "—"));
|
|
}
|
|
} catch (e) {
|
|
mount(out, h("div", { class: "callout" }, String(e.message || e)));
|
|
}
|
|
sendBtn.disabled = false;
|
|
sendBtn.textContent = t("pg.send");
|
|
});
|
|
|
|
function kv(k, v) {
|
|
return h("div", { class: "kv" }, h("span", { class: "k" }, k), h("span", { class: "v" }, v));
|
|
}
|
|
function renderDecision(r) {
|
|
const c = r.cortiq || {};
|
|
const score = c.complexity?.score ?? 0;
|
|
mount(
|
|
decision,
|
|
h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))),
|
|
kv(t("pg.task"), c.task_label || "—"),
|
|
kv(t("pg.tier"), c.complexity?.tier ? h("span", { class: "badge tier-" + c.complexity.tier }, c.complexity.tier) : "—"),
|
|
h(
|
|
"div",
|
|
{ class: "kv", style: "display:block" },
|
|
h("div", { class: "flex" }, h("span", { class: "k grow" }, t("pg.score")), h("span", { class: "v" }, score.toFixed(2))),
|
|
h("div", { class: "scorebar" }, h("i", { style: `width:${Math.min(100, score * 100)}%` }))
|
|
),
|
|
kv(t("pg.selected"), h("span", { class: "mono" }, c.selected_model || "—")),
|
|
kv(t("pg.source"), c.route_source || "—"),
|
|
kv(t("pg.tokens"), `${r.usage?.prompt_tokens ?? 0}/${r.usage?.completion_tokens ?? 0}`),
|
|
kv(t("pg.cost"), money(c.cost_usd)),
|
|
kv(t("pg.latency"), ms(r.latency_ms)),
|
|
kv(t("pg.failover"), c.failover ? "yes" : "no")
|
|
);
|
|
}
|
|
|
|
mount(
|
|
root,
|
|
h("div", { class: "page-head" }, h("h2", {}, t("pg.title")), h("p", {}, t("pg.subtitle"))),
|
|
h(
|
|
"div",
|
|
{ class: "pg" },
|
|
h(
|
|
"div",
|
|
{},
|
|
h(
|
|
"div",
|
|
{ class: "card" },
|
|
h("label", { class: "field" }, h("span", {}, t("pg.system")), sysIn),
|
|
h("label", { class: "field" }, h("span", {}, t("pg.prompt")), promptIn),
|
|
h("label", { class: "field" }, h("span", {}, t("pg.mode")), modeSeg),
|
|
modeExtra,
|
|
h("div", { class: "row" }, h("label", { class: "field" }, h("span", {}, t("pg.temp")), tempIn), h("label", { class: "field" }, h("span", {}, t("pg.maxTokens")), maxTokIn)),
|
|
h("div", { class: "flex", style: "justify-content:flex-end" }, sendBtn)
|
|
),
|
|
h("div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("pg.answer"))), out)
|
|
),
|
|
decision
|
|
)
|
|
);
|
|
return root;
|
|
}
|