// Shared helpers + window globals consumed by the JSX panels.
//
// The hardcoded INITIAL_AGENTS / SKILLS / MCPS arrays from the design have
// been removed — they now come from the live backend via api-client.jsx and
// get written onto `window.SKILLS` / `window.MCPS` / `window.AGENTS` once
// the App's initial load completes. The globals start as empty arrays so
// children that read them at first render (`SKILLS.map(...)`) don't crash.
const AGENT_COLORS = ["#10a37f", "#2563eb", "#c026d3", "#d97706", "#ef4444", "#0ea5e9", "#7c3aed"];
// LLM 模型 + 上下文窗口的**权威来源是后端**(GET /v1/models,见 models_catalog.py)。
// 下面这个 const 只是 **bootstrap fallback** —— app 一挂载就 `api.listModels()` 把
// `window.MODELS` 覆盖成后端返回的真实列表(含 context_window)。所以读窗口一律走
// `window.MODELS`(动态),不要读这个 const。
const MODELS = [
{ id: "deepseek-v4-flash", label: "deepseek-v4-flash", context_window: 655360, max_output_tokens: 409600, provider: "baizhi", providers: ["baizhi", "volcengine"] },
{ id: "glm-5.2", label: "glm-5.2", context_window: 1048576, max_output_tokens: 131072, provider: "baizhi", providers: ["baizhi", "volcengine"] },
{ id: "MiniMax/MiniMax-M2.7", label: "MiniMax/MiniMax-M2.7", context_window: 655360, provider: "baizhi" },
{ id: "qwen3.6-plus", label: "qwen3.6-plus", context_window: 655360, provider: "baizhi" },
{ id: "deepseek-v4-pro", label: "deepseek-v4-pro", context_window: 655360, provider: "baizhi" },
{ id: "MiniMax/MiniMax-M3", label: "MiniMax/MiniMax-M3", context_window: 655360, provider: "baizhi" },
{ id: "MiniMax-M2.7", label: "MiniMax-M2.7", context_window: 655360 },
{ id: "MiniMax-M2.7-highspeed", label: "MiniMax-M2.7 (highspeed)", context_window: 655360 },
];
// 找不到 model 时的兜底窗口(老 localStorage 里存了已下线 model 等场景)。
const DEFAULT_CONTEXT_WINDOW = 655360;
function modelContextWindow(modelId) {
// 优先读后端覆盖后的 window.MODELS;它还没 fetch 回来时退回 bootstrap const。
const list = (typeof window !== "undefined" && window.MODELS) ? window.MODELS : MODELS;
const m = list.find((x) => x.id === modelId);
return (m && m.context_window) || DEFAULT_CONTEXT_WINDOW;
}
function modelProviderFor(modelId) {
const list = (typeof window !== "undefined" && window.MODELS) ? window.MODELS : MODELS;
const explicitProvider = list.find((model) => model.id === modelId)?.provider;
if (explicitProvider) return explicitProvider;
const id = String(modelId || "").toLowerCase();
if (!id) return "";
if (id.startsWith("minimax-")) return "minimax";
if (id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4")) return "openai";
if (id.startsWith("claude-")) return "anthropic";
if (id.includes("/")) return "openrouter";
return "";
}
function modelOptionsForProvider(provider) {
const list = (typeof window !== "undefined" && window.MODELS) ? window.MODELS : MODELS;
const filtered = list.filter((m) => !provider || modelProviderFor(m.id) === provider || (m.providers || []).includes(provider));
return filtered;
}
function defaultModelForProvider(provider) {
const providers = window.MODEL_PROVIDERS || MODEL_PROVIDERS;
return modelOptionsForProvider(provider)[0]?.id
|| providers.find((item) => item.id === provider)?.default_model
|| MODELS[0]?.id
|| "";
}
const MODEL_PROVIDERS = [
{ id: "baizhi", label: "Baizhi", default_model: "deepseek-v4-flash", endpoint_display: "http://litellm-test.100wiser.com", credential_status: "server_managed" },
{ id: "minimax", label: "MiniMax", default_model: "MiniMax-M2.7", endpoint_display: "https://api.minimaxi.com/v1", credential_status: "server_managed" },
{ id: "openrouter", label: "OpenRouter", default_model: "openai/gpt-4o-mini", endpoint_display: "https://openrouter.ai/api/v1", credential_status: "server_managed" },
{ id: "litellm", label: "LiteLLM compatible", default_model: "deepseek-v4-flash", endpoint_display: "", credential_status: "server_managed" },
{ id: "openai", label: "OpenAI", default_model: "gpt-4o-mini", endpoint_display: "https://api.openai.com/v1", credential_status: "server_managed" },
{ id: "openai-compatible", label: "OpenAI compatible", default_model: "gpt-4o-mini", endpoint_display: "", credential_status: "server_managed" },
{ id: "volcengine", label: "volcengine", default_model: "deepseek-v4-flash", endpoint_display: "https://ark.cn-beijing.volces.com/api/plan/v3", credential_status: "server_managed" },
{ id: "anthropic", label: "Anthropic", default_model: "claude-haiku-4.5", endpoint_display: "https://api.anthropic.com", credential_status: "server_managed" },
];
window.MODEL_PROVIDERS = MODEL_PROVIDERS;
function modelEndpointForProvider(provider) {
const list = window.MODEL_PROVIDERS || MODEL_PROVIDERS;
return list.find((item) => item.id === provider)?.endpoint_display || "";
}
function modelCredentialStatusForProvider(provider) {
const list = window.MODEL_PROVIDERS || MODEL_PROVIDERS;
return list.find((item) => item.id === provider)?.credential_status || "server_managed";
}
// Start empty — api-client overwrites these once data loads.
const SKILLS = [];
const MCPS = [];
const LOCAL_TOOLS = [];
const INITIAL_AGENTS = [];
// ---------- id + time helpers ----------
function rid(prefix, len = 12) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let s = "";
for (let i = 0; i < len; i++) s += chars[Math.floor(Math.random() * chars.length)];
return prefix + "_" + s;
}
function nowHHMMSS() {
const d = new Date();
const pad = (n) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function fmtMs(ms) {
if (!Number.isFinite(ms)) return "0ms";
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
// ---------- syntax-highlight a JSON string ----------
function highlightJSON(json) {
return json
.replace(/&/g, "&").replace(//g, ">")
.replace(/("(?:\\.|[^"\\])*")(\s*:)/g, '$1$2')
.replace(/:\s*("(?:\\.|[^"\\])*")/g, ': $1')
.replace(/\b(true|false|null)\b/g, '$1')
.replace(/:\s*(-?\d+\.?\d*)/g, ': $1');
}
function escapeHTML(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function extractModelOutputMarkdown(output) {
const texts = [];
const visit = (value) => {
if (value == null) return;
if (typeof value === "string") {
if (value.trim()) texts.push(value);
return;
}
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (typeof value !== "object") return;
if (typeof value.text === "string") texts.push(value.text);
if (typeof value.content === "string") texts.push(value.content);
if (value.parts) visit(value.parts);
if (value.content && typeof value.content === "object") visit(value.content);
};
visit(output?.content?.parts || output?.parts || output?.content || output?.text || output);
return texts.join("\n\n").trim();
}
function renderMarkdownLite(md) {
const lines = String(md || "").replace(/\r\n/g, "\n").split("\n");
let html = "";
let inCode = false;
let code = [];
let inList = false;
let inTable = false;
let tableRows = [];
const inline = (text) => escapeHTML(text)
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1");
const closeList = () => {
if (inList) {
html += "";
inList = false;
}
};
const flushTable = () => {
if (!inTable) return;
if (tableRows.length) {
const [head, ...body] = tableRows;
html += "
| ${inline(c.trim())} | `).join("") + "
|---|
| ${inline(c.trim())} | `).join("") + "
${escapeHTML(code.join("\n"))}`;
inCode = false; code = [];
} else {
inCode = true; code = [];
}
continue;
}
if (inCode) {
code.push(line);
continue;
}
const cells = tableCells(line);
if (line.includes("|") && cells.length > 1) {
const nextLine = lines[i + 1] || "";
const nextCells = tableCells(nextLine);
if (inTable || isSep(nextCells)) {
closeList();
if (isSep(cells)) continue;
inTable = true;
tableRows.push(cells);
continue;
}
}
flushTable();
if (!line.trim()) {
closeList();
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) {
closeList();
const level = heading[1].length;
html += `${inline(quote[1])}`; continue; } closeList(); html += `
${inline(line)}
`; } if (inCode) html += `${escapeHTML(code.join("\n"))}`;
flushTable();
closeList();
return html;
}
// Build an AG-UI event list for a (simulated) run.
// Still used by Traces / Evals mock data; the Playground now consumes real
// backend events via api-client and renders them directly.
function buildAGUIEvents({ threadId, runId, userMessage, plan, finalText, model }) {
const events = [];
let t = Date.now();
events.push({ type: "RUN_STARTED", threadId, runId, timestamp: t, metadata: { model, source: "playground" } });
const msgId = rid("msg");
events.push({ type: "TEXT_MESSAGE_START", messageId: msgId, role: "assistant", timestamp: t += 12 });
(plan || []).forEach((step) => {
if (step.kind === "tool") {
const tcId = rid("tc");
events.push({ type: "TOOL_CALL_START", toolCallId: tcId, toolCallName: step.name, parentMessageId: msgId, timestamp: t += step.before || 80 });
events.push({ type: "TOOL_CALL_ARGS", toolCallId: tcId, toolCallName: step.name, delta: JSON.stringify(step.args || {}), timestamp: t += 4 });
events.push({ type: "TOOL_CALL_END", toolCallId: tcId, timestamp: t += 1 });
events.push({ type: "TOOL_CALL_RESULT", messageId: `tool_${tcId}`, toolCallId: tcId, role: "tool", content: { result: step.result, duration_ms: step.durMs }, timestamp: t += step.durMs });
} else if (step.kind === "llm") {
events.push({ type: "TEXT_MESSAGE_CONTENT", messageId: msgId, delta: step.chunk || "…", timestamp: t += step.durMs });
}
});
events.push({ type: "TEXT_MESSAGE_END", messageId: msgId, timestamp: t += 8 });
events.push({ type: "RUN_FINISHED", threadId, runId, timestamp: t += 4, output: { text: finalText } });
return events;
}
Object.assign(window, {
AGENT_COLORS,
MODELS,
modelContextWindow,
modelProviderFor,
modelOptionsForProvider,
defaultModelForProvider,
modelEndpointForProvider,
modelCredentialStatusForProvider,
DEFAULT_CONTEXT_WINDOW,
SKILLS,
MCPS,
LOCAL_TOOLS,
INITIAL_AGENTS,
rid,
nowHHMMSS,
fmtMs,
escapeHTML,
highlightJSON,
extractModelOutputMarkdown,
renderMarkdownLite,
buildAGUIEvents,
});