mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-27 06:14:48 -06:00
Merge pull request #316 from sillyWillieBilly/feat/console-enhancements
feat(console): theme-aware banner, judge model support, Google provider in admin
This commit is contained in:
@@ -394,7 +394,8 @@ class ClusterCollector:
|
||||
{
|
||||
"type": "ws_created",
|
||||
"ws_id": ws_id,
|
||||
"name": ws.get("name", ""),
|
||||
"name": ws.get("title", "") or ws.get("name", ""),
|
||||
"title": ws.get("title", ""),
|
||||
"node_id": node_id,
|
||||
}
|
||||
)
|
||||
@@ -418,8 +419,8 @@ class ClusterCollector:
|
||||
"content": new_w.get("content", ""),
|
||||
}
|
||||
)
|
||||
old_name = old_ws.get("name", "")
|
||||
new_name = new_w.get("name", "")
|
||||
old_name = old_ws.get("title", "") or old_ws.get("name", "")
|
||||
new_name = new_w.get("title", "") or new_w.get("name", "")
|
||||
if old_name != new_name and new_name:
|
||||
pending.append({"type": "ws_rename", "ws_id": ws_id, "name": new_name})
|
||||
node.workstreams = new_ws
|
||||
|
||||
+45
-15
@@ -129,21 +129,36 @@ _JS_PROXY_SHIM = """\
|
||||
"""
|
||||
|
||||
_CONSOLE_BANNER_TEMPLATE = (
|
||||
'<div style="background:#111827;border-bottom:1px solid rgba(229,160,66,0.3);'
|
||||
"padding:6px 20px;font-family:'IBM Plex Mono',monospace;font-size:12px;"
|
||||
'display:flex;align-items:center;gap:12px;position:relative;z-index:9999">'
|
||||
'<a href="/" style="color:#8a93ad;text-decoration:none;font-weight:500;'
|
||||
'padding:2px 0" '
|
||||
"onmouseover=\"this.style.color='#e5a042'\" "
|
||||
"onmouseout=\"this.style.color='#8a93ad'\">"
|
||||
"← Console</a>"
|
||||
'<span style="color:#3b4463">\u2502</span>'
|
||||
'<span style="color:#8a93ad;font-size:11px">NODE_ID_PLACEHOLDER</span>'
|
||||
'<div class="console-banner">'
|
||||
'<a href="/" class="console-banner-link">← Console</a>'
|
||||
'<span class="console-banner-sep">\u2502</span>'
|
||||
'<a href="NODE_LINK_PLACEHOLDER" class="console-banner-node">'
|
||||
"NODE_ID_PLACEHOLDER</a>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
# Injected <style> offsets fixed-position overlays below the console banner.
|
||||
_CONSOLE_PROXY_STYLE = "<style>.dashboard-overlay{top:32px!important}</style>"
|
||||
# Injected <style>: offsets fixed-position overlays and provides theme-aware
|
||||
# banner styling so the banner adapts to light/dark without inline colours.
|
||||
_CONSOLE_PROXY_STYLE = (
|
||||
"<style>"
|
||||
".dashboard-overlay{top:32px!important}"
|
||||
".console-banner{background:#111827;border-bottom:1px solid rgba(229,160,66,0.3);"
|
||||
"padding:6px 20px;font-family:'IBM Plex Mono',monospace;font-size:12px;"
|
||||
"display:flex;align-items:center;gap:12px;position:relative;z-index:9999}"
|
||||
".console-banner-link,.console-banner-node{color:#8a93ad;text-decoration:none}"
|
||||
".console-banner-link{font-weight:500;padding:2px 0}"
|
||||
".console-banner-node{font-size:11px}"
|
||||
".console-banner-sep{color:#3b4463}"
|
||||
".console-banner-link:hover,.console-banner-node:hover{color:#e5a042}"
|
||||
':root[data-theme="light"] .console-banner{background:#f8fafc;'
|
||||
"border-bottom-color:rgba(229,160,66,0.5)}"
|
||||
':root[data-theme="light"] .console-banner-link,'
|
||||
':root[data-theme="light"] .console-banner-node{color:#64748b}'
|
||||
':root[data-theme="light"] .console-banner-sep{color:#cbd5e1}'
|
||||
':root[data-theme="light"] .console-banner-link:hover,'
|
||||
':root[data-theme="light"] .console-banner-node:hover{color:#e5a042}'
|
||||
"</style>"
|
||||
)
|
||||
|
||||
|
||||
_VALID_NODE_ID = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||
@@ -446,6 +461,7 @@ async def create_workstream(request: Request) -> JSONResponse:
|
||||
raw_node_id = body.get("node_id", "")
|
||||
raw_name = body.get("name", "")
|
||||
raw_model = body.get("model", "")
|
||||
raw_judge_model = body.get("judge_model", "")
|
||||
raw_initial_message = body.get("initial_message", "")
|
||||
raw_skill = body.get("skill", "")
|
||||
raw_resume_ws = body.get("resume_ws", "")
|
||||
@@ -455,6 +471,8 @@ async def create_workstream(request: Request) -> JSONResponse:
|
||||
raw_name = "" if raw_name is None else None
|
||||
if not isinstance(raw_model, str):
|
||||
raw_model = "" if raw_model is None else None
|
||||
if not isinstance(raw_judge_model, str):
|
||||
raw_judge_model = "" if raw_judge_model is None else None
|
||||
if not isinstance(raw_initial_message, str):
|
||||
raw_initial_message = "" if raw_initial_message is None else None
|
||||
if not isinstance(raw_skill, str):
|
||||
@@ -465,19 +483,21 @@ async def create_workstream(request: Request) -> JSONResponse:
|
||||
raw_node_id is None
|
||||
or raw_name is None
|
||||
or raw_model is None
|
||||
or raw_judge_model is None
|
||||
or raw_initial_message is None
|
||||
or raw_skill is None
|
||||
or raw_resume_ws is None
|
||||
):
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "node_id, name, model, initial_message, skill, and resume_ws must be strings"
|
||||
"error": "node_id, name, model, judge_model, initial_message, skill, and resume_ws must be strings"
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
node_id = raw_node_id
|
||||
name = raw_name[:256]
|
||||
model = raw_model[:128]
|
||||
judge_model = raw_judge_model[:128]
|
||||
initial_message = raw_initial_message[:4096]
|
||||
skill = raw_skill[:256]
|
||||
resume_ws = raw_resume_ws[:64]
|
||||
@@ -509,6 +529,7 @@ async def create_workstream(request: Request) -> JSONResponse:
|
||||
ws_body = {
|
||||
"name": name,
|
||||
"model": model,
|
||||
"judge_model": judge_model,
|
||||
"initial_message": initial_message,
|
||||
"skill": skill,
|
||||
"resume_ws": resume_ws,
|
||||
@@ -918,7 +939,7 @@ async def proxy_index(request: Request) -> Response:
|
||||
page = page.replace('href="/shared/', f'href="{prefix}/shared/')
|
||||
page = page.replace('src="/shared/', f'src="{prefix}/shared/')
|
||||
# Inject console-return banner + proxy shim after <body>
|
||||
banner = _CONSOLE_BANNER_TEMPLATE.replace("NODE_ID_PLACEHOLDER", html.escape(node_id))
|
||||
banner = _CONSOLE_BANNER_TEMPLATE.replace("NODE_ID_PLACEHOLDER", html.escape(node_id)).replace("NODE_LINK_PLACEHOLDER", html.escape(prefix + "/"))
|
||||
shim = (
|
||||
"<script>"
|
||||
+ _JS_PROXY_SHIM.replace('"PREFIX_PLACEHOLDER"', json.dumps(prefix))
|
||||
@@ -5151,7 +5172,12 @@ async def admin_import_mcp_config(request: Request) -> JSONResponse:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MODEL_ALIAS_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||
_MODEL_PROVIDERS = frozenset({"openai", "anthropic", "openai-compatible"})
|
||||
_MODEL_PROVIDERS = frozenset({"openai", "anthropic", "openai-compatible", "google"})
|
||||
_PROVIDER_DEFAULT_URLS: dict[str, str] = {
|
||||
"openai": "https://api.openai.com/v1",
|
||||
"anthropic": "https://api.anthropic.com",
|
||||
"google": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
}
|
||||
|
||||
|
||||
def _mask_model_secrets(model: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -5591,6 +5617,10 @@ async def admin_detect_model(request: Request) -> JSONResponse:
|
||||
if not base_url:
|
||||
base_url = row.get("base_url", "")
|
||||
|
||||
# Apply provider default URL if still empty
|
||||
if not base_url:
|
||||
base_url = _PROVIDER_DEFAULT_URLS.get(provider, "")
|
||||
|
||||
# For commercial endpoints an api_key is required
|
||||
_normalized = (base_url if "://" in base_url else f"https://{base_url}") if base_url else ""
|
||||
_hostname = (urllib.parse.urlparse(_normalized).hostname or "") if _normalized else ""
|
||||
|
||||
@@ -403,8 +403,8 @@ function confirmDeleteUser(userId, username) {
|
||||
showConfirmModal(
|
||||
"Delete User",
|
||||
"Delete user \u2018" +
|
||||
username +
|
||||
"\u2019 and all their tokens and channel links? This cannot be undone.",
|
||||
username +
|
||||
"\u2019 and all their tokens and channel links? This cannot be undone.",
|
||||
"Delete",
|
||||
function () {
|
||||
authFetch("/v1/api/admin/users/" + encodeURIComponent(userId), {
|
||||
@@ -570,19 +570,19 @@ function _confirmUnlinkOidc(issuer, subject, username, userId) {
|
||||
showConfirmModal(
|
||||
"Unlink OIDC Identity",
|
||||
"Unlink " +
|
||||
shortIssuer +
|
||||
" identity \u2018" +
|
||||
shortSubject +
|
||||
"\u2019 from user " +
|
||||
username +
|
||||
"?\n\nThe user will need to log in via OIDC again to re-link.",
|
||||
shortIssuer +
|
||||
" identity \u2018" +
|
||||
shortSubject +
|
||||
"\u2019 from user " +
|
||||
username +
|
||||
"?\n\nThe user will need to log in via OIDC again to re-link.",
|
||||
"Unlink",
|
||||
function () {
|
||||
authFetch(
|
||||
"/v1/api/admin/oidc-identities?issuer=" +
|
||||
encodeURIComponent(issuer) +
|
||||
"&subject=" +
|
||||
encodeURIComponent(subject),
|
||||
encodeURIComponent(issuer) +
|
||||
"&subject=" +
|
||||
encodeURIComponent(subject),
|
||||
{ method: "DELETE" },
|
||||
)
|
||||
.then(function (r) {
|
||||
@@ -608,8 +608,8 @@ function _confirmUnlinkOidc(issuer, subject, username, userId) {
|
||||
'<span class="oidc-detail-empty">Loading\u2026</span>';
|
||||
authFetch(
|
||||
"/v1/api/admin/users/" +
|
||||
encodeURIComponent(userId) +
|
||||
"/oidc-identities",
|
||||
encodeURIComponent(userId) +
|
||||
"/oidc-identities",
|
||||
)
|
||||
.then(function (r2) {
|
||||
if (!r2.ok) throw new Error("Failed");
|
||||
@@ -878,17 +878,17 @@ function confirmUnlinkChannel(channelType, channelUserId) {
|
||||
showConfirmModal(
|
||||
"Unlink Channel",
|
||||
"Unlink " +
|
||||
channelType +
|
||||
" account \u2018" +
|
||||
channelUserId +
|
||||
"\u2019? The user will need to re-link via /link to interact with the bot.",
|
||||
channelType +
|
||||
" account \u2018" +
|
||||
channelUserId +
|
||||
"\u2019? The user will need to re-link via /link to interact with the bot.",
|
||||
"Unlink",
|
||||
function () {
|
||||
authFetch(
|
||||
"/v1/api/admin/channels/" +
|
||||
encodeURIComponent(channelType) +
|
||||
"/" +
|
||||
encodeURIComponent(channelUserId),
|
||||
encodeURIComponent(channelType) +
|
||||
"/" +
|
||||
encodeURIComponent(channelUserId),
|
||||
{ method: "DELETE" },
|
||||
)
|
||||
.then(function (r) {
|
||||
@@ -1059,8 +1059,8 @@ function confirmDeleteSchedule(taskId, name) {
|
||||
showConfirmModal(
|
||||
"Delete Schedule",
|
||||
"Delete schedule \u2018" +
|
||||
name +
|
||||
"\u2019 and its run history? This cannot be undone.",
|
||||
name +
|
||||
"\u2019 and its run history? This cannot be undone.",
|
||||
"Delete",
|
||||
function () {
|
||||
authFetch("/v1/api/admin/schedules/" + encodeURIComponent(taskId), {
|
||||
@@ -1671,12 +1671,12 @@ function _renderWatches(watches) {
|
||||
var statusDot = active ? "\u25cf " : "\u25cb ";
|
||||
var cancelBtn = active
|
||||
? '<button class="admin-btn-danger" data-cancel-watch="' +
|
||||
escapeHtml(w.watch_id) +
|
||||
'" data-watch-node="' +
|
||||
escapeHtml(w.node_id || "") +
|
||||
'" data-watch-name="' +
|
||||
escapeHtml(name) +
|
||||
'" title="Cancel watch">cancel</button>'
|
||||
escapeHtml(w.watch_id) +
|
||||
'" data-watch-node="' +
|
||||
escapeHtml(w.node_id || "") +
|
||||
'" data-watch-name="' +
|
||||
escapeHtml(name) +
|
||||
'" title="Cancel watch">cancel</button>'
|
||||
: "";
|
||||
html +=
|
||||
'<div class="admin-row" role="listitem">' +
|
||||
@@ -1797,8 +1797,8 @@ function submitCreateChannel() {
|
||||
|
||||
authFetch(
|
||||
"/v1/api/admin/users/" +
|
||||
encodeURIComponent(_adminChannelUserId) +
|
||||
"/channels",
|
||||
encodeURIComponent(_adminChannelUserId) +
|
||||
"/channels",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -3044,6 +3044,22 @@ function _saveSettingValue(key) {
|
||||
showToast(
|
||||
"Saved " + key + (restartBadge ? " \u2014 restart required" : ""),
|
||||
);
|
||||
|
||||
// If this is a theme setting, apply it immediately
|
||||
if (key === "interface.theme") {
|
||||
var newTheme = value;
|
||||
document.documentElement.dataset.theme =
|
||||
newTheme === "light" ? "light" : "";
|
||||
localStorage.setItem(
|
||||
"turnstone-theme",
|
||||
newTheme === "light" ? "light" : "dark",
|
||||
);
|
||||
var themeBtn = document.getElementById("theme-toggle");
|
||||
if (themeBtn) {
|
||||
themeBtn.textContent =
|
||||
newTheme === "light" ? "\u2600" : "\u263E";
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (saveBtn) {
|
||||
@@ -3058,8 +3074,8 @@ function _resetSetting(key) {
|
||||
showConfirmModal(
|
||||
"Reset Setting",
|
||||
"Reset \u2018" +
|
||||
key +
|
||||
"\u2019 to its default value? The stored override will be removed.",
|
||||
key +
|
||||
"\u2019 to its default value? The stored override will be removed.",
|
||||
"Reset",
|
||||
function () {
|
||||
authFetch("/v1/api/admin/settings/" + encodeURIComponent(key), {
|
||||
@@ -3202,13 +3218,13 @@ function _renderMcpServers(items) {
|
||||
var actions = isConfig
|
||||
? ""
|
||||
: '<button class="admin-btn-action" data-mcp-edit="' +
|
||||
escapeHtml(s.server_id) +
|
||||
'">edit</button>' +
|
||||
'<button class="admin-btn-danger" data-mcp-delete="' +
|
||||
escapeHtml(s.server_id) +
|
||||
'" data-mcp-name="' +
|
||||
escapeHtml(s.name) +
|
||||
'">del</button>';
|
||||
escapeHtml(s.server_id) +
|
||||
'">edit</button>' +
|
||||
'<button class="admin-btn-danger" data-mcp-delete="' +
|
||||
escapeHtml(s.server_id) +
|
||||
'" data-mcp-name="' +
|
||||
escapeHtml(s.name) +
|
||||
'">del</button>';
|
||||
|
||||
html +=
|
||||
'<div class="admin-row mcp-grid ' +
|
||||
@@ -3404,11 +3420,11 @@ function _parseMcpForm() {
|
||||
var argsText = document.getElementById("mcp-args").value.trim();
|
||||
payload.args = argsText
|
||||
? argsText
|
||||
.split("\n")
|
||||
.map(function (l) {
|
||||
return l.trim();
|
||||
})
|
||||
.filter(Boolean)
|
||||
.split("\n")
|
||||
.map(function (l) {
|
||||
return l.trim();
|
||||
})
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
var envText = document.getElementById("mcp-env").value.trim();
|
||||
var envObj = {};
|
||||
@@ -3560,7 +3576,7 @@ function _openMcpDetail(s) {
|
||||
'<p style="font-size:12px;color:var(--fg-dim)">Args: <code>' +
|
||||
escapeHtml(a.join(" ")) +
|
||||
"</code></p>";
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
} else {
|
||||
html +=
|
||||
'<p style="font-size:12px;color:var(--fg-dim)">URL: <code>' +
|
||||
@@ -3599,7 +3615,7 @@ function _openMcpDetail(s) {
|
||||
escapeHtml(meta.website_url) +
|
||||
"</a></p>";
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
html += "</div>";
|
||||
}
|
||||
html += "</div>";
|
||||
@@ -3891,8 +3907,8 @@ function _renderRegistryResults() {
|
||||
"</div>" +
|
||||
(srv.description
|
||||
? '<div class="mcp-reg-card-desc">' +
|
||||
escapeHtml(srv.description) +
|
||||
"</div>"
|
||||
escapeHtml(srv.description) +
|
||||
"</div>"
|
||||
: "") +
|
||||
'<div class="mcp-reg-card-meta">' +
|
||||
sourceBadges +
|
||||
@@ -3900,8 +3916,8 @@ function _renderRegistryResults() {
|
||||
'<div class="mcp-reg-card-actions">' +
|
||||
(srv.version
|
||||
? '<span class="mcp-reg-card-version">v' +
|
||||
escapeHtml(srv.version) +
|
||||
"</span>"
|
||||
escapeHtml(srv.version) +
|
||||
"</span>"
|
||||
: "") +
|
||||
actionHtml +
|
||||
"</div></div>";
|
||||
@@ -3924,9 +3940,9 @@ function _renderRegistryResults() {
|
||||
moreBtn.style.display = "";
|
||||
countEl.textContent = isFiltered
|
||||
? visibleCount +
|
||||
" of " +
|
||||
_registryResults.length +
|
||||
" loaded (more available)"
|
||||
" of " +
|
||||
_registryResults.length +
|
||||
" loaded (more available)"
|
||||
: "Showing " + visibleCount + " results";
|
||||
} else {
|
||||
pagEl.style.display = visibleCount > 0 ? "" : "none";
|
||||
@@ -4004,8 +4020,8 @@ function _showInstallMcpModal(srv, hasRemote, hasPackage) {
|
||||
"</div>" +
|
||||
(srv.description
|
||||
? '<div class="mcp-install-summary-desc">' +
|
||||
escapeHtml(srv.description) +
|
||||
"</div>"
|
||||
escapeHtml(srv.description) +
|
||||
"</div>"
|
||||
: "");
|
||||
|
||||
// Source selector (only if both remote AND package)
|
||||
@@ -4325,7 +4341,7 @@ function _pollInstallStatus(serverId, serverName, attempt) {
|
||||
_pollInstallStatus(serverId, serverName, attempt + 1);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
.catch(function () { });
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -4570,6 +4586,7 @@ function showCreateModelModal() {
|
||||
document.getElementById("model-detect-btn").disabled = false;
|
||||
document.getElementById("model-detect-btn").textContent = "Detect";
|
||||
_refreshModelSuggestions();
|
||||
_applyProviderDefaults();
|
||||
document.getElementById("model-alias").focus();
|
||||
_modelCreateTrap = _installTrap("model-create-overlay", "model-create-box");
|
||||
}
|
||||
@@ -4606,6 +4623,7 @@ function showEditModelModal(definitionId) {
|
||||
if (caps === "{}") caps = "";
|
||||
document.getElementById("model-capabilities").value = caps;
|
||||
document.getElementById("model-enabled").checked = m.enabled !== false;
|
||||
_applyProviderDefaults();
|
||||
})
|
||||
.catch(function () {
|
||||
showToast("Failed to load model details");
|
||||
@@ -4780,6 +4798,18 @@ function detectModel() {
|
||||
}
|
||||
resultDiv.appendChild(_detectResultLine(msg, "yellow"));
|
||||
}
|
||||
|
||||
if (d.available_models && d.available_models.length > 0) {
|
||||
var dl = document.getElementById("model-name-suggestions");
|
||||
if (dl) {
|
||||
dl.textContent = "";
|
||||
d.available_models.forEach(function (m) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = m;
|
||||
dl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (d.context_window) {
|
||||
resultDiv.appendChild(
|
||||
_detectResultLine(
|
||||
@@ -4826,9 +4856,9 @@ function _onModelFieldChange() {
|
||||
if (!modelName) return;
|
||||
authFetch(
|
||||
"/v1/api/admin/model-capabilities?provider=" +
|
||||
encodeURIComponent(provider) +
|
||||
"&model=" +
|
||||
encodeURIComponent(modelName),
|
||||
encodeURIComponent(provider) +
|
||||
"&model=" +
|
||||
encodeURIComponent(modelName),
|
||||
)
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
@@ -4859,6 +4889,23 @@ function _onModelFieldChange() {
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
/* Provider-specific placeholder hints for base_url and model ID fields. */
|
||||
var _providerDefaults = {
|
||||
openai: { urlPlaceholder: "https://api.openai.com/v1", modelPlaceholder: "gpt-5" },
|
||||
anthropic: { urlPlaceholder: "https://api.anthropic.com", modelPlaceholder: "claude-" },
|
||||
google: { urlPlaceholder: "https://generativelanguage.googleapis.com/v1beta/openai/", modelPlaceholder: "gemini-" },
|
||||
"openai-compatible": { urlPlaceholder: "e.g. https://your-provider.com/v1", modelPlaceholder: "GLM5" },
|
||||
};
|
||||
|
||||
/* Update placeholders when provider changes. */
|
||||
function _applyProviderDefaults() {
|
||||
var provider = document.getElementById("model-provider").value;
|
||||
var def = _providerDefaults[provider];
|
||||
if (!def) return;
|
||||
document.getElementById("model-base-url").placeholder = def.urlPlaceholder;
|
||||
document.getElementById("model-name").placeholder = def.modelPlaceholder;
|
||||
}
|
||||
|
||||
/* Populate the model name datalist with known model prefixes for the
|
||||
selected provider. Called on page load and provider change. */
|
||||
function _refreshModelSuggestions() {
|
||||
@@ -4867,7 +4914,7 @@ function _refreshModelSuggestions() {
|
||||
var provider = document.getElementById("model-provider").value;
|
||||
authFetch(
|
||||
"/v1/api/admin/model-capabilities/known?provider=" +
|
||||
encodeURIComponent(provider),
|
||||
encodeURIComponent(provider),
|
||||
)
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
@@ -4894,6 +4941,7 @@ function _refreshModelSuggestions() {
|
||||
provEl.addEventListener("change", _onModelFieldChange);
|
||||
provEl.addEventListener("change", _refreshModelSuggestions);
|
||||
provEl.addEventListener("change", _clearDetectResult);
|
||||
provEl.addEventListener("change", _applyProviderDefaults);
|
||||
}
|
||||
/* Clear stale detect results when probe-relevant inputs change */
|
||||
["model-base-url", "model-api-key"].forEach(function (id) {
|
||||
|
||||
@@ -11,6 +11,13 @@ window.onLogout = function () {
|
||||
window.onThemeChange = function (next) {
|
||||
var btn = document.getElementById("theme-toggle");
|
||||
if (btn) btn.textContent = next === "light" ? "\u2600" : "\u263E";
|
||||
// Persist to server so admin settings and node UIs see the change
|
||||
var themeValue = next === "light" ? "light" : "dark";
|
||||
authFetch("/v1/api/admin/settings/interface.theme", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value: themeValue }),
|
||||
}).catch(function () {});
|
||||
};
|
||||
// Set initial theme button text
|
||||
(function () {
|
||||
@@ -1114,7 +1121,7 @@ function renderWsTable(container, wsList) {
|
||||
// NAME
|
||||
var nameCell = document.createElement("span");
|
||||
nameCell.className = "dash-cell-name";
|
||||
nameCell.textContent = ws.name || ws.id || "";
|
||||
nameCell.textContent = ws.name || ws.title || ws.id || "";
|
||||
main.appendChild(nameCell);
|
||||
|
||||
// MODEL
|
||||
@@ -1281,11 +1288,20 @@ function showNewWsModal() {
|
||||
});
|
||||
// Populate model dropdown
|
||||
var modelSelect = document.getElementById("new-ws-model");
|
||||
var judgeSelect = document.getElementById("new-ws-judge");
|
||||
modelSelect.textContent = "";
|
||||
judgeSelect.textContent = "";
|
||||
|
||||
var defaultOpt = document.createElement("option");
|
||||
defaultOpt.value = "";
|
||||
defaultOpt.textContent = "Default model";
|
||||
modelSelect.appendChild(defaultOpt);
|
||||
|
||||
var defaultJudgeOpt = document.createElement("option");
|
||||
defaultJudgeOpt.value = "";
|
||||
defaultJudgeOpt.textContent = "Default (same as agent)";
|
||||
judgeSelect.appendChild(defaultJudgeOpt);
|
||||
|
||||
authFetch("/v1/api/models")
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
@@ -1297,6 +1313,11 @@ function showNewWsModal() {
|
||||
opt.textContent =
|
||||
m.alias === m.model ? m.alias : m.alias + " (" + m.model + ")";
|
||||
modelSelect.appendChild(opt);
|
||||
|
||||
var jOpt = document.createElement("option");
|
||||
jOpt.value = m.alias;
|
||||
jOpt.textContent = opt.textContent;
|
||||
judgeSelect.appendChild(jOpt);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
@@ -1304,6 +1325,7 @@ function showNewWsModal() {
|
||||
});
|
||||
document.getElementById("new-ws-name").value = "";
|
||||
modelSelect.value = "";
|
||||
judgeSelect.value = "";
|
||||
var taskEl = document.getElementById("new-ws-task");
|
||||
taskEl.value = "";
|
||||
var mod =
|
||||
@@ -1363,6 +1385,7 @@ function submitNewWs() {
|
||||
var nodeId = document.getElementById("new-ws-node").value;
|
||||
var name = document.getElementById("new-ws-name").value.trim();
|
||||
var model = document.getElementById("new-ws-model").value.trim();
|
||||
var judgeModel = document.getElementById("new-ws-judge").value.trim();
|
||||
var skill = document.getElementById("new-ws-skill").value;
|
||||
var task = document.getElementById("new-ws-task").value.trim();
|
||||
var errEl = document.getElementById("new-ws-error");
|
||||
@@ -1376,6 +1399,7 @@ function submitNewWs() {
|
||||
if (nodeId) body.node_id = nodeId;
|
||||
if (name) body.name = name;
|
||||
if (model) body.model = model;
|
||||
if (judgeModel) body.judge_model = judgeModel;
|
||||
if (task) body.initial_message = task;
|
||||
if (skill) body.skill = skill;
|
||||
|
||||
|
||||
@@ -791,6 +791,10 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<select id="new-ws-skill">
|
||||
<option value="">Use defaults</option>
|
||||
</select>
|
||||
<label for="new-ws-judge">Judge Model <span class="label-hint">optional</span></label>
|
||||
<select id="new-ws-judge">
|
||||
<option value="">Default (same as agent)</option>
|
||||
</select>
|
||||
<div id="new-ws-buttons">
|
||||
<button id="new-ws-cancel" onclick="hideNewWsModal()">Cancel</button>
|
||||
<button id="new-ws-submit" onclick="submitNewWs()">Create</button>
|
||||
@@ -1521,6 +1525,7 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<select id="model-provider">
|
||||
<option value="openai">openai</option>
|
||||
<option value="anthropic">anthropic</option>
|
||||
<option value="google">google</option>
|
||||
<option value="openai-compatible">openai-compatible</option>
|
||||
</select>
|
||||
<label for="model-base-url">Base URL <span style="font-weight:400;text-transform:none">(empty = provider default)</span></label>
|
||||
|
||||
Reference in New Issue
Block a user