mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
Polish Admin/Chat UI and fix WhatsApp group session titles.
Surface group names in channel titles, theme composer chips for light mode, and trim unused Runtime alert/stack panels. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8ad4768476
commit
746570dfc9
8 changed files with 1659 additions and 619 deletions
|
|
@ -52,6 +52,18 @@ const I18N = {
|
|||
"scheduledJobs.editTitle": "编辑任务",
|
||||
"scheduledJobs.updated": "已保存",
|
||||
"scheduledJobs.cancel": "取消",
|
||||
"scheduledJobs.fieldName": "名称",
|
||||
"scheduledJobs.fieldSchedule": "计划",
|
||||
"scheduledJobs.fieldKind": "类型",
|
||||
"scheduledJobs.fieldExpr": "表达式",
|
||||
"scheduledJobs.fieldPrompt": "提示词",
|
||||
"scheduledJobs.fieldSpecialist": "专家",
|
||||
"scheduledJobs.fieldWhatsapp": "WhatsApp chat_id",
|
||||
"scheduledJobs.phName": "任务名称",
|
||||
"scheduledJobs.phExpr": "cron / ISO 时间 / 秒数",
|
||||
"scheduledJobs.phPrompt": "到期时要执行的提示词",
|
||||
"scheduledJobs.phSpecialist": "例如 generalist",
|
||||
"scheduledJobs.phWhatsapp": "可选,群或私聊 chat_id",
|
||||
"scheduledJobs.runHistory": "运行记录",
|
||||
"scheduledJobs.runHistoryHint": "最近运行(自动刷新)",
|
||||
"scheduledJobs.noRuns": "暂无运行记录",
|
||||
|
|
@ -535,6 +547,18 @@ const I18N = {
|
|||
"scheduledJobs.editTitle": "Edit job",
|
||||
"scheduledJobs.updated": "Saved.",
|
||||
"scheduledJobs.cancel": "Cancel",
|
||||
"scheduledJobs.fieldName": "Name",
|
||||
"scheduledJobs.fieldSchedule": "Schedule",
|
||||
"scheduledJobs.fieldKind": "Kind",
|
||||
"scheduledJobs.fieldExpr": "Expression",
|
||||
"scheduledJobs.fieldPrompt": "Prompt",
|
||||
"scheduledJobs.fieldSpecialist": "Specialist",
|
||||
"scheduledJobs.fieldWhatsapp": "WhatsApp chat_id",
|
||||
"scheduledJobs.phName": "Job name",
|
||||
"scheduledJobs.phExpr": "cron / ISO time / seconds",
|
||||
"scheduledJobs.phPrompt": "Prompt to run when due",
|
||||
"scheduledJobs.phSpecialist": "e.g. generalist",
|
||||
"scheduledJobs.phWhatsapp": "Optional group or DM chat_id",
|
||||
"scheduledJobs.runHistory": "Run history",
|
||||
"scheduledJobs.runHistoryHint": "Recent runs (auto refresh)",
|
||||
"scheduledJobs.noRuns": "No runs yet",
|
||||
|
|
@ -1518,17 +1542,30 @@ function renderPageShell(opts = {}, children = []) {
|
|||
}
|
||||
|
||||
function renderSectionCard(title, subtitle, bodyNodes = [], opts = {}) {
|
||||
const nodes = [el("div", { class: "card__title", text: title })];
|
||||
if (subtitle) nodes.push(el("div", { class: "muted", text: subtitle }));
|
||||
const headlineKids = [el("div", { class: "card__title", text: title })];
|
||||
if (subtitle) headlineKids.push(el("div", { class: "card__subtitle muted", text: subtitle }));
|
||||
const headKids = [el("div", { class: "card__headline" }, headlineKids)];
|
||||
if (Array.isArray(opts.actions) && opts.actions.length) {
|
||||
nodes.push(el("div", { class: "row section-card__actions" }, opts.actions));
|
||||
headKids.push(el("div", { class: "card__actions row" }, opts.actions));
|
||||
}
|
||||
const nodes = [el("div", { class: "card__head" }, headKids)];
|
||||
bodyNodes.filter(Boolean).forEach((node) => nodes.push(node));
|
||||
const attrs = { class: "card section-card" };
|
||||
if (opts.id) attrs.id = String(opts.id);
|
||||
return el("div", attrs, nodes);
|
||||
}
|
||||
|
||||
function renderMetaChips(entries) {
|
||||
const items = [];
|
||||
const src = entries && typeof entries === "object" ? entries : {};
|
||||
Object.keys(src).forEach((k) => {
|
||||
const v = src[k];
|
||||
if (v == null || v === "") return;
|
||||
items.push(el("span", { class: "meta-chip", text: `${k}=${v}` }));
|
||||
});
|
||||
return el("div", { class: "meta-line" }, items.length ? items : [el("span", { class: "muted", text: "—" })]);
|
||||
}
|
||||
|
||||
function shortText(v, maxLen = 200) {
|
||||
const s = String(v ?? "");
|
||||
if (s.length <= maxLen) return s;
|
||||
|
|
@ -1579,8 +1616,6 @@ function markPrewarmReminder(reason) {
|
|||
|
||||
async function renderStack() {
|
||||
const results = await Promise.allSettled([
|
||||
apiGetNoHang("/admin/api/stack/status"),
|
||||
apiGetNoHang("/admin/api/runtime/anomalies"),
|
||||
apiGetNoHang("/admin/api/runtime/scan-artifacts"),
|
||||
apiGetNoHang("/admin/api/runtime/prewarm/status"),
|
||||
apiGetNoHang("/admin/api/runtime/prewarm/prompts?role=generalist"),
|
||||
|
|
@ -1590,64 +1625,22 @@ async function renderStack() {
|
|||
apiGetNoHang("/admin/api/whatsapp/groups?tenant_id=default"),
|
||||
apiGetNoHang("/admin/api/whatsapp/access?tenant_id=default"),
|
||||
]);
|
||||
const st = results[0].status === "fulfilled" ? results[0].value : null;
|
||||
const anomaliesResp = results[1].status === "fulfilled" ? results[1].value : null;
|
||||
const scanResp = results[2].status === "fulfilled" ? results[2].value : null;
|
||||
const prewarmStatusResp = results[3].status === "fulfilled" ? results[3].value : null;
|
||||
const prewarmPromptsResp = results[4].status === "fulfilled" ? results[4].value : null;
|
||||
const channelSpecResp = results[5].status === "fulfilled" ? results[5].value : null;
|
||||
const weixinDispatchResp = results[6].status === "fulfilled" ? results[6].value : null;
|
||||
const whatsappDispatchResp = results[7].status === "fulfilled" ? results[7].value : null;
|
||||
const whatsappGroupsResp = results[8].status === "fulfilled" ? results[8].value : null;
|
||||
const whatsappAccessResp = results[9].status === "fulfilled" ? results[9].value : null;
|
||||
const scanResp = results[0].status === "fulfilled" ? results[0].value : null;
|
||||
const prewarmStatusResp = results[1].status === "fulfilled" ? results[1].value : null;
|
||||
const prewarmPromptsResp = results[2].status === "fulfilled" ? results[2].value : null;
|
||||
const channelSpecResp = results[3].status === "fulfilled" ? results[3].value : null;
|
||||
const weixinDispatchResp = results[4].status === "fulfilled" ? results[4].value : null;
|
||||
const whatsappDispatchResp = results[5].status === "fulfilled" ? results[5].value : null;
|
||||
const whatsappGroupsResp = results[6].status === "fulfilled" ? results[6].value : null;
|
||||
const whatsappAccessResp = results[7].status === "fulfilled" ? results[7].value : null;
|
||||
|
||||
// If auth failed (401), show a gentle message instead of an infinite spinner.
|
||||
if (!st) {
|
||||
if (!channelSpecResp && !weixinDispatchResp && !prewarmStatusResp) {
|
||||
return el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: t("title.stack") || "Stack" }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "未登录或会话已过期,请重新登录。" : "Not logged in or session expired. Please log in again." }),
|
||||
]);
|
||||
}
|
||||
const requiredServices = ["gateway", "channel:wecom"];
|
||||
const runningNames = new Set(
|
||||
(Array.isArray(st.items) ? st.items : [])
|
||||
.filter((x) => Boolean(x && x.running))
|
||||
.map((x) => String(x.name || "")),
|
||||
);
|
||||
const missingRequired = requiredServices.filter((n) => !runningNames.has(n));
|
||||
const items = (st.items || []).flatMap((x) => {
|
||||
const name = String((x && x.name) || "");
|
||||
const running = Boolean(x && x.running);
|
||||
const allPids = Array.isArray(x && x.all_pids) ? x.all_pids.map((v) => String(v || "").trim()).filter(Boolean) : [];
|
||||
const primary = String((x && x.pid) || "").trim();
|
||||
const pidPorts = (x && x.pid_ports && typeof x.pid_ports === "object") ? x.pid_ports : {};
|
||||
const pidRunning = (x && x.pid_running && typeof x.pid_running === "object") ? x.pid_running : {};
|
||||
const rows = allPids.length ? allPids : (primary ? [primary] : []);
|
||||
return rows.map((pid) => {
|
||||
const ports = Array.isArray(pidPorts[pid]) ? pidPorts[pid].map((v) => String(v || "").trim()).filter(Boolean) : [];
|
||||
const isPrimary = primary && pid === primary;
|
||||
const isRunning = Boolean(pidRunning[pid]);
|
||||
return el("tr", {}, [
|
||||
el("td", {}, [
|
||||
el("div", { text: pid || "-" }),
|
||||
isPrimary ? el("div", { class: "muted", text: "primary" }) : el("span"),
|
||||
]),
|
||||
el("td", { text: name }),
|
||||
el("td", { text: ports.length ? ports.join(", ") : "-" }),
|
||||
el("td", {}, [
|
||||
el("span", { class: "badge " + (isRunning ? "badge--ok" : "badge--bad"), text: isRunning ? t("status.running") : t("status.stopped") }),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
const btnUp = el("button", { class: "btn btn--primary", text: t("stack.up"), onclick: async () => {
|
||||
await apiPost("/admin/api/stack/up", { channel: "wecom" });
|
||||
router();
|
||||
}});
|
||||
const btnDown = el("button", { class: "btn btn--danger", text: t("stack.down"), onclick: async () => {
|
||||
await apiPost("/admin/api/stack/down", {});
|
||||
router();
|
||||
}});
|
||||
const availableDispatchSpecialists = Array.isArray(channelSpecResp && channelSpecResp.available_specialists) && channelSpecResp.available_specialists.length
|
||||
? channelSpecResp.available_specialists.map((x) => String(x || "").trim()).filter(Boolean)
|
||||
: ["generalist"];
|
||||
|
|
@ -1663,7 +1656,11 @@ async function renderStack() {
|
|||
el("option", { value: "zh", text: currentLang === "zh" ? "中文" : "Chinese", selected: curLang === "zh" ? "selected" : undefined }),
|
||||
el("option", { value: "en", text: currentLang === "zh" ? "英文" : "English", selected: curLang === "en" ? "selected" : undefined }),
|
||||
]);
|
||||
const status = el("div", { class: "muted", text: `mode=${curMode} specialist=${curSpecialist} lang=${curLang}` });
|
||||
const status = renderMetaChips({ mode: curMode, specialist: curSpecialist, lang: curLang });
|
||||
const paintStatus = (obj) => {
|
||||
const fresh = renderMetaChips(obj);
|
||||
status.replaceChildren(...Array.from(fresh.childNodes));
|
||||
};
|
||||
const saveExpertBtn = el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "绑定专家" : "Bind specialist",
|
||||
|
|
@ -1675,20 +1672,35 @@ async function renderStack() {
|
|||
specialist,
|
||||
lang,
|
||||
});
|
||||
status.textContent = `mode=${String(resp.interaction_mode || "expert")} specialist=${String(resp.specialist || specialist)} lang=${String(resp.lang || lang)}`;
|
||||
paintStatus({
|
||||
mode: String(resp.interaction_mode || "expert"),
|
||||
specialist: String(resp.specialist || specialist),
|
||||
lang: String(resp.lang || lang),
|
||||
});
|
||||
},
|
||||
});
|
||||
return el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: title }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: currentLang === "zh" ? "专家" : "Specialist" }),
|
||||
specialistSel,
|
||||
el("label", { text: currentLang === "zh" ? "语言" : "Lang" }),
|
||||
langSel,
|
||||
saveExpertBtn,
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: title }),
|
||||
el("div", {
|
||||
class: "card__subtitle",
|
||||
text: currentLang === "zh" ? "通道统一走专家模式;选择默认专家即可。" : "Channels are expert-only; pick the default specialist.",
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "专家" : "Specialist" }),
|
||||
specialistSel,
|
||||
]),
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "语言" : "Lang" }),
|
||||
langSel,
|
||||
]),
|
||||
el("div", { class: "form-inline__actions" }, [saveExpertBtn]),
|
||||
]),
|
||||
status,
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "通道统一走专家模式;选择默认专家即可。" : "Channels are expert-only; pick the default specialist." }),
|
||||
]);
|
||||
};
|
||||
const weixinDispatchCard = createChannelDispatchCard("weixin", "Weixin dispatch", weixinDispatchResp || {});
|
||||
|
|
@ -1753,9 +1765,14 @@ async function renderStack() {
|
|||
else if (lt === "whitelist") waCounts.whitelist += 1;
|
||||
else if (lt === "blacklist") waCounts.blacklist += 1;
|
||||
});
|
||||
const waAccessStatus = el("div", {
|
||||
class: "muted",
|
||||
text: `mode=${String(waAccessCfg.access_mode || "blacklist")} lang=${String(waAccessCfg.lang || "en")} admin=${waCounts.admin} whitelist=${waCounts.whitelist} blacklist=${waCounts.blacklist} pending=${waPending.length} denied=${waDenied.length}`,
|
||||
const waAccessStatus = renderMetaChips({
|
||||
mode: String(waAccessCfg.access_mode || "blacklist"),
|
||||
lang: String(waAccessCfg.lang || "en"),
|
||||
admin: waCounts.admin,
|
||||
whitelist: waCounts.whitelist,
|
||||
blacklist: waCounts.blacklist,
|
||||
pending: waPending.length,
|
||||
denied: waDenied.length,
|
||||
});
|
||||
const waContactFilterSel = el("select", { class: "input" }, [
|
||||
el("option", { value: "all", text: currentLang === "zh" ? "全部" : "All" }),
|
||||
|
|
@ -1979,156 +1996,177 @@ async function renderStack() {
|
|||
]);
|
||||
});
|
||||
const whatsappAccessCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "WhatsApp 访问控制" : "WhatsApp access control" }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "默认黑名单模式:未授权用户进入待审批;拒绝后进入黑名单/已拒绝列表,可一键加白名单。" : "Default blacklist mode: unauthorized users go to Pending; denied users go to blacklist/denied list and can be whitelisted." }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: currentLang === "zh" ? "模式" : "Mode" }),
|
||||
waAccessModeSel,
|
||||
el("label", { text: currentLang === "zh" ? "提示语言" : "Message lang" }),
|
||||
waAccessLangSel,
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "保存配置" : "Save config",
|
||||
onclick: async () => {
|
||||
const resp = await apiPost("/admin/api/whatsapp/access/config", {
|
||||
tenant_id: "default",
|
||||
access_mode: String(waAccessModeSel.value || "blacklist"),
|
||||
lang: String(waAccessLangSel.value || "en"),
|
||||
});
|
||||
const cfg = (resp && resp.config) || {};
|
||||
waAccessStatus.textContent = `mode=${String(cfg.access_mode || "blacklist")} lang=${String(cfg.lang || "en")}`;
|
||||
},
|
||||
}),
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "WhatsApp 访问控制" : "WhatsApp access control" }),
|
||||
el("div", {
|
||||
class: "card__subtitle",
|
||||
text: currentLang === "zh" ? "默认黑名单模式:未授权用户进入待审批;拒绝后进入黑名单/已拒绝列表,可一键加白名单。" : "Default blacklist mode: unauthorized users go to Pending; denied users go to blacklist/denied list and can be whitelisted.",
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "模式" : "Mode" }),
|
||||
waAccessModeSel,
|
||||
]),
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "提示语言" : "Message lang" }),
|
||||
waAccessLangSel,
|
||||
]),
|
||||
el("div", { class: "form-inline__actions" }, [
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "保存配置" : "Save config",
|
||||
onclick: async () => {
|
||||
const resp = await apiPost("/admin/api/whatsapp/access/config", {
|
||||
tenant_id: "default",
|
||||
access_mode: String(waAccessModeSel.value || "blacklist"),
|
||||
lang: String(waAccessLangSel.value || "en"),
|
||||
});
|
||||
const cfg = (resp && resp.config) || {};
|
||||
waAccessStatus.replaceChildren(
|
||||
...Array.from(
|
||||
renderMetaChips({
|
||||
mode: String(cfg.access_mode || "blacklist"),
|
||||
lang: String(cfg.lang || "en"),
|
||||
admin: waCounts.admin,
|
||||
whitelist: waCounts.whitelist,
|
||||
blacklist: waCounts.blacklist,
|
||||
pending: waPending.length,
|
||||
denied: waDenied.length,
|
||||
}).childNodes,
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
waAccessStatus,
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: currentLang === "zh" ? "筛选" : "Filter" }),
|
||||
waContactFilterSel,
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "筛选" : "Filter" }),
|
||||
waContactFilterSel,
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "row" }, [waPendingPickSel]),
|
||||
el("div", { class: "row" }, [waContactPhoneInput, waContactNameInput, waContactTypeSel]),
|
||||
el("div", { class: "row" }, [
|
||||
el("button", {
|
||||
class: "btn",
|
||||
text: currentLang === "zh" ? "添加联系人" : "Add contact",
|
||||
onclick: async () => {
|
||||
const phone = String(waContactPhoneInput.value || "").trim();
|
||||
if (!phone) return;
|
||||
try {
|
||||
const resp = await apiPost("/admin/api/whatsapp/access/contacts", {
|
||||
phone,
|
||||
push_name: String(waContactNameInput.value || "").trim(),
|
||||
list_type: String(waContactTypeSel.value || "whitelist"),
|
||||
});
|
||||
if (!resp || resp.ok === false) {
|
||||
window.alert(String((resp && resp.error) || (currentLang === "zh" ? "添加失败" : "Add failed")));
|
||||
return;
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field form-inline__field--wide" }, [waPendingPickSel]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [waContactPhoneInput]),
|
||||
el("div", { class: "form-inline__field" }, [waContactNameInput]),
|
||||
el("div", { class: "form-inline__field" }, [waContactTypeSel]),
|
||||
el("div", { class: "form-inline__actions" }, [
|
||||
el("button", {
|
||||
class: "btn",
|
||||
text: currentLang === "zh" ? "添加联系人" : "Add contact",
|
||||
onclick: async () => {
|
||||
const phone = String(waContactPhoneInput.value || "").trim();
|
||||
if (!phone) return;
|
||||
try {
|
||||
const resp = await apiPost("/admin/api/whatsapp/access/contacts", {
|
||||
phone,
|
||||
push_name: String(waContactNameInput.value || "").trim(),
|
||||
list_type: String(waContactTypeSel.value || "whitelist"),
|
||||
});
|
||||
if (!resp || resp.ok === false) {
|
||||
window.alert(String((resp && resp.error) || (currentLang === "zh" ? "添加失败" : "Add failed")));
|
||||
return;
|
||||
}
|
||||
router();
|
||||
} catch (err) {
|
||||
window.alert(String(err));
|
||||
}
|
||||
router();
|
||||
} catch (err) {
|
||||
window.alert(String(err));
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "类型" : "Type" }),
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
waContactsTbody,
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "类型" : "Type" }),
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
waContactsTbody,
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "待审批请求" : "Pending requests" }),
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: "JID" }),
|
||||
el("th", { text: currentLang === "zh" ? "消息" : "Message" }),
|
||||
el("th", { text: currentLang === "zh" ? "时间" : "Time" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
el("tbody", {}, waPendingRows.length ? waPendingRows : [el("tr", {}, [el("td", { colspan: "6", text: currentLang === "zh" ? "无待审批" : "None" })])]),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: "JID" }),
|
||||
el("th", { text: currentLang === "zh" ? "消息" : "Message" }),
|
||||
el("th", { text: currentLang === "zh" ? "时间" : "Time" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
el("tbody", {}, waPendingRows.length ? waPendingRows : [el("tr", {}, [el("td", { colspan: "6", text: currentLang === "zh" ? "无待审批" : "None" })])]),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "已拒绝 / 黑名单记录" : "Denied / blacklist records" }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "点「拒绝」后用户会进入黑名单联系人,并保留在此列表;可一键加白名单恢复访问。" : "Denied users are blacklisted and listed here; use Whitelist to restore access." }),
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: currentLang === "zh" ? "消息" : "Message" }),
|
||||
el("th", { text: currentLang === "zh" ? "拒绝时间" : "Denied at" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
el("tbody", {}, waDeniedRows.length ? waDeniedRows : [el("tr", {}, [el("td", { colspan: "5", text: currentLang === "zh" ? "无已拒绝记录" : "None" })])]),
|
||||
el("div", { class: "card__hint", text: currentLang === "zh" ? "点「拒绝」后用户会进入黑名单联系人,并保留在此列表;可一键加白名单恢复访问。" : "Denied users are blacklisted and listed here; use Whitelist to restore access." }),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: currentLang === "zh" ? "名称" : "Name" }),
|
||||
el("th", { text: currentLang === "zh" ? "电话" : "Phone" }),
|
||||
el("th", { text: currentLang === "zh" ? "消息" : "Message" }),
|
||||
el("th", { text: currentLang === "zh" ? "拒绝时间" : "Denied at" }),
|
||||
el("th", { text: currentLang === "zh" ? "操作" : "Action" }),
|
||||
])]),
|
||||
el("tbody", {}, waDeniedRows.length ? waDeniedRows : [el("tr", {}, [el("td", { colspan: "5", text: currentLang === "zh" ? "无已拒绝记录" : "None" })])]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
const whatsappAlertBindingCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "WhatsApp 告警群绑定" : "WhatsApp alert group" }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "NetX 关键告警将推送到此群;与 Chat 会话删除无关。" : "NetX key alerts go to this group; independent of chat sessions." }),
|
||||
el("div", { class: "row" }, [waGroupSel]),
|
||||
el("div", { class: "row" }, [
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "保存绑定" : "Save binding",
|
||||
onclick: async () => {
|
||||
const group_jid = String(waGroupSel.value || "").trim();
|
||||
if (!group_jid) return;
|
||||
const picked = waGroups.find((g) => String((g && g.group_jid) || "").trim() === group_jid) || {};
|
||||
const resp = await apiPost("/admin/api/whatsapp/alert-binding", {
|
||||
tenant_id: "default",
|
||||
group_jid,
|
||||
group_name: String((picked && picked.group_name) || ""),
|
||||
enabled: true,
|
||||
});
|
||||
const b = (resp && resp.binding) || {};
|
||||
waBindingStatus.textContent = b.group_jid
|
||||
? `binding=${b.group_jid} enabled=${Boolean(b.enabled)}`
|
||||
: (currentLang === "zh" ? "绑定失败" : "Bind failed");
|
||||
},
|
||||
}),
|
||||
el("button", {
|
||||
class: "btn",
|
||||
text: currentLang === "zh" ? "测试推送" : "Test push",
|
||||
onclick: async () => {
|
||||
const resp = await apiPost("/admin/api/whatsapp/alert-binding/test", { tenant_id: "default" });
|
||||
waBindingStatus.textContent = resp && resp.ok
|
||||
? `test ok outbound_id=${String(resp.outbound_id || "")}`
|
||||
: `test failed: ${String((resp && resp.error) || "unknown")}`;
|
||||
},
|
||||
}),
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "WhatsApp 告警群绑定" : "WhatsApp alert group" }),
|
||||
el("div", {
|
||||
class: "card__subtitle",
|
||||
text: currentLang === "zh" ? "NetX 关键告警将推送到此群;与 Chat 会话删除无关。" : "NetX key alerts go to this group; independent of chat sessions.",
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field form-inline__field--wide" }, [waGroupSel]),
|
||||
el("div", { class: "form-inline__actions" }, [
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "保存绑定" : "Save binding",
|
||||
onclick: async () => {
|
||||
const group_jid = String(waGroupSel.value || "").trim();
|
||||
if (!group_jid) return;
|
||||
const picked = waGroups.find((g) => String((g && g.group_jid) || "").trim() === group_jid) || {};
|
||||
const resp = await apiPost("/admin/api/whatsapp/alert-binding", {
|
||||
tenant_id: "default",
|
||||
group_jid,
|
||||
group_name: String((picked && picked.group_name) || ""),
|
||||
enabled: true,
|
||||
});
|
||||
const b = (resp && resp.binding) || {};
|
||||
waBindingStatus.textContent = b.group_jid
|
||||
? `binding=${b.group_jid} enabled=${Boolean(b.enabled)}`
|
||||
: (currentLang === "zh" ? "绑定失败" : "Bind failed");
|
||||
},
|
||||
}),
|
||||
el("button", {
|
||||
class: "btn",
|
||||
text: currentLang === "zh" ? "测试推送" : "Test push",
|
||||
onclick: async () => {
|
||||
const resp = await apiPost("/admin/api/whatsapp/alert-binding/test", { tenant_id: "default" });
|
||||
waBindingStatus.textContent = resp && resp.ok
|
||||
? `test ok outbound_id=${String(resp.outbound_id || "")}`
|
||||
: `test failed: ${String((resp && resp.error) || "unknown")}`;
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
waBindingStatus,
|
||||
]);
|
||||
const cleanupStatus = el("div", { class: "muted", text: "" });
|
||||
const btnCleanup = el("button", { class: "btn btn--danger", text: t("stack.cleanup"), onclick: async () => {
|
||||
const resp = await apiPost("/admin/api/runtime/cleanup", {});
|
||||
const killed = Array.isArray(resp.killed) ? resp.killed : [];
|
||||
const total = killed.reduce((acc, x) => acc + ((x && Array.isArray(x.killed_pids)) ? x.killed_pids.length : 0), 0);
|
||||
cleanupStatus.textContent = total > 0 ? tf("stack.cleanupDone", { count: total }) : t("stack.cleanupNone");
|
||||
router();
|
||||
}});
|
||||
const alerts = Array.isArray(anomaliesResp.items) ? anomaliesResp.items : [];
|
||||
const mustCleanup = Boolean(anomaliesResp.must_cleanup);
|
||||
const mergedAlerts = missingRequired.length
|
||||
? []
|
||||
: alerts.filter((a) => {
|
||||
const typ = String((a && a.type) || "");
|
||||
return typ !== "duplicate_process_ambiguous";
|
||||
});
|
||||
if (missingRequired.length) {
|
||||
mergedAlerts.unshift({
|
||||
severity: "critical",
|
||||
message: tf("stack.missingServices", { services: missingRequired.join(", ") }),
|
||||
});
|
||||
}
|
||||
const alertRows = mergedAlerts.length
|
||||
? mergedAlerts.map((a) => el("li", {
|
||||
text: `[${String(a.severity || "info").toUpperCase()}] ${String(a.message || "")}`,
|
||||
}))
|
||||
: [el("li", { text: t("stack.noAlerts") })];
|
||||
const scanItems = Array.isArray(scanResp && scanResp.items) ? scanResp.items : [];
|
||||
const scanDir = String((scanResp && scanResp.dir) || "");
|
||||
const scanStatus = el("div", { class: "muted", text: "" });
|
||||
|
|
@ -2269,88 +2307,92 @@ async function renderStack() {
|
|||
renderPromptCards(resp);
|
||||
});
|
||||
renderPromptCards(prewarmPromptsResp);
|
||||
return el("div", {}, [
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: t("stack.alerts") }),
|
||||
(mustCleanup || missingRequired.length)
|
||||
? el("div", {
|
||||
class: "alert alert--critical",
|
||||
text: missingRequired.length ? t("stack.missingRequired") : t("stack.mustCleanup"),
|
||||
})
|
||||
: el("div", { class: "muted", text: t("stack.noAlerts") }),
|
||||
el("ul", { class: "alert-list" }, alertRows),
|
||||
missingRequired.length ? el("div", { class: "muted", text: t("stack.missingRequired") }) : el("div", { class: "row" }, [btnCleanup]),
|
||||
cleanupStatus,
|
||||
return el("div", { class: "page-shell" }, [
|
||||
el("div", { class: "page-grid page-grid--two" }, [
|
||||
weixinDispatchCard,
|
||||
whatsappDispatchCard,
|
||||
]),
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: t("stack.title") }),
|
||||
el("div", { class: "row" }, [btnUp, btnDown]),
|
||||
el("div", { class: "u-h-10" }),
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [el("th", { text: t("table.pid") }), el("th", { text: t("table.service") }), el("th", { text: "端口" }), el("th", { text: t("table.status") })])]),
|
||||
el("tbody", {}, items),
|
||||
]),
|
||||
]),
|
||||
weixinDispatchCard,
|
||||
whatsappDispatchCard,
|
||||
whatsappAccessCard,
|
||||
whatsappAlertBindingCard,
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "提示词/工具预热" : "Prompt/Tool Prewarm" }),
|
||||
el("div", { class: "muted", text: prewarmSummary }),
|
||||
runtimePrewarmReminder ? el("div", { class: "alert alert--warning", text: runtimePrewarmReminder }) : el("span"),
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "提示词/工具预热" : "Prompt/Tool Prewarm" }),
|
||||
el("div", { class: "card__subtitle", text: prewarmSummary }),
|
||||
]),
|
||||
el("div", { class: "card__actions" }, [btnPrewarm]),
|
||||
]),
|
||||
runtimePrewarmReminder ? el("div", { class: "alert alert--warning", text: runtimePrewarmReminder }) : null,
|
||||
el(
|
||||
"div",
|
||||
{
|
||||
class: "muted",
|
||||
class: "card__hint",
|
||||
text:
|
||||
currentLang === "zh"
|
||||
? "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。系统每10分钟自动异步预热一次。"
|
||||
: "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. System also auto-prewarms every 10 minutes.",
|
||||
},
|
||||
),
|
||||
el("div", { class: "row" }, [btnPrewarm]),
|
||||
prewarmStatus,
|
||||
]),
|
||||
].filter(Boolean)),
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "预热历史(最近20次)" : "Prewarm History (latest 20)" }),
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: "finished_at" }),
|
||||
el("th", { text: "reason" }),
|
||||
el("th", { text: "ok" }),
|
||||
el("th", { text: "elapsed_ms" }),
|
||||
el("th", { text: "error" }),
|
||||
])]),
|
||||
el("tbody", {}, historyRows),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: "finished_at" }),
|
||||
el("th", { text: "reason" }),
|
||||
el("th", { text: "ok" }),
|
||||
el("th", { text: "elapsed_ms" }),
|
||||
el("th", { text: "error" }),
|
||||
])]),
|
||||
el("tbody", {}, historyRows),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "预热后提示词(按专家)" : "Prewarmed Prompts by Role" }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "展示各专家预热后的提示词内容。" : "Shows prewarmed prompt content for specialists." }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: currentLang === "zh" ? "角色筛选" : "Role filter" }),
|
||||
promptsRoleSelect,
|
||||
btnReloadPrompts,
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: currentLang === "zh" ? "预热后提示词(按专家)" : "Prewarmed Prompts by Role" }),
|
||||
el("div", {
|
||||
class: "card__subtitle",
|
||||
text: currentLang === "zh" ? "展示各专家预热后的提示词内容。" : "Shows prewarmed prompt content for specialists.",
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: currentLang === "zh" ? "角色筛选" : "Role filter" }),
|
||||
promptsRoleSelect,
|
||||
]),
|
||||
el("div", { class: "form-inline__actions" }, [btnReloadPrompts]),
|
||||
]),
|
||||
promptsSectionBody,
|
||||
]),
|
||||
el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: "扫描缓存文件" }),
|
||||
el("div", { class: "muted", text: scanDir || "-" }),
|
||||
el("div", { class: "muted", text: "仅清理 history_entries_*.json 与 state_scan_*.json" }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "keep_latest" }),
|
||||
keepLatestInput,
|
||||
el("label", { text: "max_age_days" }),
|
||||
maxAgeDaysInput,
|
||||
btnPruneScan,
|
||||
btnCleanScan,
|
||||
el("div", { class: "card__head" }, [
|
||||
el("div", { class: "card__headline" }, [
|
||||
el("div", { class: "card__title", text: "扫描缓存文件" }),
|
||||
el("div", { class: "card__subtitle", text: `${scanDir || "-"} · 仅清理 history_entries_*.json 与 state_scan_*.json` }),
|
||||
]),
|
||||
]),
|
||||
el("div", { class: "form-inline" }, [
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: "keep_latest" }),
|
||||
keepLatestInput,
|
||||
]),
|
||||
el("div", { class: "form-inline__field" }, [
|
||||
el("label", { text: "max_age_days" }),
|
||||
maxAgeDaysInput,
|
||||
]),
|
||||
el("div", { class: "form-inline__actions" }, [btnPruneScan, btnCleanScan]),
|
||||
]),
|
||||
scanStatus,
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [el("th", { text: "name" }), el("th", { text: "bytes" }), el("th", { text: "modified_at" })])]),
|
||||
el("tbody", {}, scanRows),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [el("th", { text: "name" }), el("th", { text: "bytes" }), el("th", { text: "modified_at" })])]),
|
||||
el("tbody", {}, scanRows),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
|
@ -4135,7 +4177,7 @@ async function renderModels() {
|
|||
secretsCard.style.display = "none";
|
||||
return;
|
||||
}
|
||||
secretsCard.style.display = "";
|
||||
secretsCard.style.display = "block";
|
||||
if (!canMigrate) {
|
||||
secretsMigrateBtn.disabled = true;
|
||||
secretsStatusMsg.textContent = t("secrets.migrateForbidden");
|
||||
|
|
@ -4478,7 +4520,7 @@ async function renderModels() {
|
|||
|
||||
modelsGrantsLinkRow.innerHTML = "";
|
||||
if (state.can_manage_llm_grants) {
|
||||
modelsGrantsLinkRow.style.display = "";
|
||||
modelsGrantsLinkRow.style.display = "block";
|
||||
modelsGrantsLinkRow.appendChild(el("span", { text: `${t("models.grantsNavHint")} ` }));
|
||||
modelsGrantsLinkRow.appendChild(el("a", { href: "#/api-grants", text: t("models.linkApiGrants") }));
|
||||
} else {
|
||||
|
|
@ -5908,13 +5950,13 @@ async function renderPlugins() {
|
|||
healthBtn.className = "chat-sess-menu-item";
|
||||
syncBtn.className = "chat-sess-menu-item";
|
||||
const actionMenuBtn = el("button", {
|
||||
class: "chat-sess-more u-overlay-fixed",
|
||||
class: "chat-sess-more",
|
||||
text: "⋯",
|
||||
title: "MCP actions",
|
||||
onclick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
closeMcpActionMenu();
|
||||
const menu = el("div", { class: "chat-sess-menu-pop" }, [
|
||||
const menu = el("div", { class: "chat-sess-menu-pop u-overlay-fixed" }, [
|
||||
toggleBtn,
|
||||
healthBtn,
|
||||
syncBtn,
|
||||
|
|
@ -8757,13 +8799,13 @@ async function renderSkills() {
|
|||
},
|
||||
});
|
||||
const actionMenuBtn = el("button", {
|
||||
class: "chat-sess-more u-overlay-fixed",
|
||||
class: "chat-sess-more",
|
||||
text: "⋯",
|
||||
title: "Skill actions",
|
||||
onclick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
closeSkillActionMenu();
|
||||
const menu = el("div", { class: "chat-sess-menu-pop" }, [
|
||||
const menu = el("div", { class: "chat-sess-menu-pop u-overlay-fixed" }, [
|
||||
toggleBtn,
|
||||
testRunBtn,
|
||||
repairDepsBtn,
|
||||
|
|
@ -9115,7 +9157,7 @@ async function renderScheduledJobs() {
|
|||
scheduledJobMenuEl = null;
|
||||
};
|
||||
|
||||
const status = el("select", {}, [
|
||||
const status = el("select", { class: "input input--compact" }, [
|
||||
el("option", { value: "", text: t("scheduledJobs.all") }),
|
||||
el("option", { value: "active", text: t("scheduledJobs.statusActive") }),
|
||||
el("option", { value: "paused", text: t("scheduledJobs.statusPaused") }),
|
||||
|
|
@ -9124,18 +9166,24 @@ async function renderScheduledJobs() {
|
|||
const runsBox = el("pre", { class: "code", style: "max-height:220px;overflow:auto;white-space:pre-wrap;" });
|
||||
const msg = el("div", { class: "muted", text: "" });
|
||||
|
||||
const field = (label, control, wide) =>
|
||||
el("div", { class: wide ? "form-field form-field--full" : "form-field" }, [
|
||||
el("label", { class: "form-field__label", text: label }),
|
||||
control,
|
||||
]);
|
||||
|
||||
const editModal = el("div", { class: "session-monitor-modal u-hidden" });
|
||||
const editTitle = el("div", { class: "card__title", text: t("scheduledJobs.editTitle") });
|
||||
const editNameInput = el("input", { class: "input", placeholder: "name" });
|
||||
const editKindInput = el("select", {}, [
|
||||
const editNameInput = el("input", { class: "input", placeholder: t("scheduledJobs.phName") });
|
||||
const editKindInput = el("select", { class: "input" }, [
|
||||
el("option", { value: "cron", text: "cron" }),
|
||||
el("option", { value: "once", text: "once" }),
|
||||
el("option", { value: "interval", text: "interval" }),
|
||||
]);
|
||||
const editExprInput = el("input", { class: "input", placeholder: "schedule_expr (cron / ISO / seconds)" });
|
||||
const editPromptInput = el("textarea", { class: "input", rows: "4", placeholder: "prompt_text" });
|
||||
const editSpecialistInput = el("input", { class: "input", placeholder: "specialist" });
|
||||
const editWaChatInput = el("input", { class: "input", placeholder: "whatsapp chat_id (optional)" });
|
||||
const editExprInput = el("input", { class: "input", placeholder: t("scheduledJobs.phExpr") });
|
||||
const editPromptInput = el("textarea", { class: "input", rows: "5", placeholder: t("scheduledJobs.phPrompt") });
|
||||
const editSpecialistInput = el("input", { class: "input", placeholder: t("scheduledJobs.phSpecialist") });
|
||||
const editWaChatInput = el("input", { class: "input", placeholder: t("scheduledJobs.phWhatsapp") });
|
||||
const editWeixinInfo = el("div", { class: "muted", text: "" });
|
||||
const closeEditModal = () => {
|
||||
editingJob = null;
|
||||
|
|
@ -9194,14 +9242,21 @@ async function renderScheduledJobs() {
|
|||
const editModalCard = el("div", { class: "card session-monitor-modal__card", style: "width:min(640px,96vw);" }, [
|
||||
editTitle,
|
||||
editWeixinInfo,
|
||||
editNameInput,
|
||||
el("div", { class: "row u-gap-8" }, [editKindInput, editExprInput]),
|
||||
editPromptInput,
|
||||
editSpecialistInput,
|
||||
editWaChatInput,
|
||||
el("div", { class: "row u-row-end" }, [
|
||||
el("button", { class: "btn", text: t("scheduledJobs.cancel"), onclick: closeEditModal }),
|
||||
editSaveBtn,
|
||||
el("div", { class: "form-stack" }, [
|
||||
field(t("scheduledJobs.fieldName"), editNameInput, true),
|
||||
el("div", { class: "form-grid" }, [
|
||||
field(t("scheduledJobs.fieldKind"), editKindInput),
|
||||
field(t("scheduledJobs.fieldExpr"), editExprInput),
|
||||
]),
|
||||
field(t("scheduledJobs.fieldPrompt"), editPromptInput, true),
|
||||
el("div", { class: "form-grid" }, [
|
||||
field(t("scheduledJobs.fieldSpecialist"), editSpecialistInput),
|
||||
field(t("scheduledJobs.fieldWhatsapp"), editWaChatInput),
|
||||
]),
|
||||
el("div", { class: "form-actions" }, [
|
||||
el("button", { class: "btn", text: t("scheduledJobs.cancel"), onclick: closeEditModal }),
|
||||
editSaveBtn,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
editModal.appendChild(editModalCard);
|
||||
|
|
@ -9235,13 +9290,13 @@ async function renderScheduledJobs() {
|
|||
for (const job of resp.items || []) {
|
||||
const jobId = String(job.id || "");
|
||||
const btnMore = el("button", {
|
||||
class: "chat-sess-more u-overlay-fixed",
|
||||
class: "chat-sess-more",
|
||||
text: "⋯",
|
||||
title: t("scheduledJobs.menuTitle"),
|
||||
onclick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
closeScheduledJobMenu();
|
||||
const menu = el("div", { class: "chat-sess-menu-pop" }, [
|
||||
const menu = el("div", { class: "chat-sess-menu-pop u-overlay-fixed" }, [
|
||||
el("button", {
|
||||
class: "chat-sess-menu-item",
|
||||
text: t("scheduledJobs.viewRuns"),
|
||||
|
|
@ -9353,16 +9408,16 @@ async function renderScheduledJobs() {
|
|||
await loadLatestRuns(resp.items || []);
|
||||
}
|
||||
|
||||
const nameInput = el("input", { class: "input", placeholder: "name" });
|
||||
const kindInput = el("select", {}, [
|
||||
const nameInput = el("input", { class: "input", placeholder: t("scheduledJobs.phName") });
|
||||
const kindInput = el("select", { class: "input" }, [
|
||||
el("option", { value: "cron", text: "cron" }),
|
||||
el("option", { value: "once", text: "once" }),
|
||||
el("option", { value: "interval", text: "interval" }),
|
||||
]);
|
||||
const exprInput = el("input", { class: "input", placeholder: "schedule_expr (cron / ISO / seconds)" });
|
||||
const promptInput = el("textarea", { class: "input", rows: "3", placeholder: "prompt_text" });
|
||||
const specialistInput = el("input", { class: "input", placeholder: "specialist", value: "generalist" });
|
||||
const waChatInput = el("input", { class: "input", placeholder: "whatsapp chat_id (optional)" });
|
||||
const exprInput = el("input", { class: "input", placeholder: t("scheduledJobs.phExpr") });
|
||||
const promptInput = el("textarea", { class: "input", rows: "5", placeholder: t("scheduledJobs.phPrompt") });
|
||||
const specialistInput = el("input", { class: "input", placeholder: t("scheduledJobs.phSpecialist"), value: "generalist" });
|
||||
const waChatInput = el("input", { class: "input", placeholder: t("scheduledJobs.phWhatsapp") });
|
||||
const weixinInfo = el("div", { class: "muted", text: "" });
|
||||
|
||||
async function loadMeta() {
|
||||
|
|
@ -9379,27 +9434,36 @@ async function renderScheduledJobs() {
|
|||
const createCardChildren = [
|
||||
el("div", { class: "card__title", text: t("scheduledJobs.createTitle") }),
|
||||
weixinInfo,
|
||||
nameInput,
|
||||
el("div", { class: "row u-gap-8" }, [kindInput, exprInput]),
|
||||
promptInput,
|
||||
specialistInput,
|
||||
waChatInput,
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: t("scheduledJobs.create"),
|
||||
onclick: async () => {
|
||||
await apiPost("/admin/api/scheduled-jobs", {
|
||||
name: nameInput.value,
|
||||
schedule_kind: kindInput.value,
|
||||
schedule_expr: exprInput.value,
|
||||
prompt_text: promptInput.value,
|
||||
specialist: specialistInput.value,
|
||||
whatsapp_chat_id: waChatInput.value,
|
||||
delivery: buildScheduledJobDeliveryPayload(null, waChatInput.value),
|
||||
});
|
||||
await loadJobs();
|
||||
},
|
||||
}),
|
||||
el("div", { class: "form-stack" }, [
|
||||
field(t("scheduledJobs.fieldName"), nameInput, true),
|
||||
el("div", { class: "form-grid" }, [
|
||||
field(t("scheduledJobs.fieldKind"), kindInput),
|
||||
field(t("scheduledJobs.fieldExpr"), exprInput),
|
||||
]),
|
||||
field(t("scheduledJobs.fieldPrompt"), promptInput, true),
|
||||
el("div", { class: "form-grid" }, [
|
||||
field(t("scheduledJobs.fieldSpecialist"), specialistInput),
|
||||
field(t("scheduledJobs.fieldWhatsapp"), waChatInput),
|
||||
]),
|
||||
el("div", { class: "form-actions" }, [
|
||||
el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: t("scheduledJobs.create"),
|
||||
onclick: async () => {
|
||||
await apiPost("/admin/api/scheduled-jobs", {
|
||||
name: nameInput.value,
|
||||
schedule_kind: kindInput.value,
|
||||
schedule_expr: exprInput.value,
|
||||
prompt_text: promptInput.value,
|
||||
specialist: specialistInput.value,
|
||||
whatsapp_chat_id: waChatInput.value,
|
||||
delivery: buildScheduledJobDeliveryPayload(null, waChatInput.value),
|
||||
});
|
||||
await loadJobs();
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
|
||||
return el("div", {}, [
|
||||
|
|
|
|||
|
|
@ -51,18 +51,19 @@
|
|||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
min-height: 40px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
.chat-sess-row:hover {
|
||||
background: var(--chat-sess-hover-bg, rgba(255, 255, 255, 0.05));
|
||||
border-color: var(--chat-sess-border, rgba(255, 255, 255, 0.09));
|
||||
border-color: transparent;
|
||||
}
|
||||
.chat-sess-row--active {
|
||||
background: var(--chat-sess-active-bg, rgba(255, 255, 255, 0.062));
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: var(--chat-sess-active-bg, rgba(148, 163, 184, 0.12));
|
||||
border-color: transparent;
|
||||
}
|
||||
.chat-sess-row .chat-sess-btn {
|
||||
flex: 1;
|
||||
|
|
@ -117,7 +118,8 @@
|
|||
background: transparent;
|
||||
}
|
||||
.chat-sess-menu-pop {
|
||||
z-index: 200;
|
||||
/* Must stay above .u-overlay-scrim (250) or menu clicks hit the transparent scrim. */
|
||||
z-index: 300;
|
||||
background: var(--ds-panel, var(--ds-surface, #222));
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.12));
|
||||
border-radius: 8px;
|
||||
|
|
@ -260,16 +262,16 @@
|
|||
opacity: 0.9;
|
||||
}
|
||||
.chat-att-ref {
|
||||
min-width: 180px;
|
||||
max-width: 260px;
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.14));
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chat-att-ref__name {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
margin-bottom: 4px;
|
||||
|
|
@ -281,32 +283,42 @@
|
|||
opacity: 0.75;
|
||||
word-break: break-all;
|
||||
}
|
||||
.chat-att-ref__link {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--ds-accent, #5eb3ff);
|
||||
text-decoration: none;
|
||||
}
|
||||
.chat-att-ref__link:hover {
|
||||
text-decoration: underline;
|
||||
.chat-att-ref__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.chat-att-ref__action,
|
||||
.chat-att-ref__link,
|
||||
.chat-att-ref__btn {
|
||||
margin-top: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.14));
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: inherit;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--ds-text, #e8e8e8);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
padding: 4px 8px;
|
||||
padding: 4px 9px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-att-ref__action:hover,
|
||||
.chat-att-ref__link:hover,
|
||||
.chat-att-ref__btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
text-decoration: none;
|
||||
}
|
||||
.chat-att-ref__action:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.chat-att-ref__preview {
|
||||
margin: 6px 0 0;
|
||||
margin: 8px 0 0;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.14));
|
||||
|
|
@ -321,10 +333,64 @@
|
|||
.chat-att-ref__preview:empty {
|
||||
display: none;
|
||||
}
|
||||
.chat-composer-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 4px 8px;
|
||||
}
|
||||
.chat-composer-chip,
|
||||
.chat-composer-chip--select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
max-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.28));
|
||||
background-color: var(--ds-surface-2, #152033);
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-composer-chip--select {
|
||||
min-width: 148px;
|
||||
max-width: 240px;
|
||||
padding-right: 28px;
|
||||
background-color: var(--ds-surface-2, #152033);
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%),
|
||||
linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 14px) calc(50% - 2px),
|
||||
calc(100% - 9px) calc(50% - 2px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.chat-composer-chip--select option {
|
||||
background-color: var(--ds-surface, #0f172a);
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
}
|
||||
.chat-composer-chip:hover,
|
||||
.chat-composer-chip--select:hover {
|
||||
background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16));
|
||||
border-color: var(--ds-accent-border, rgba(94, 179, 255, 0.35));
|
||||
}
|
||||
.chat-composer-chip:focus-visible,
|
||||
.chat-composer-chip--select:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--ds-accent-border-strong, rgba(94, 179, 255, 0.5));
|
||||
box-shadow: 0 0 0 2px var(--ds-accent-soft, rgba(94, 179, 255, 0.2));
|
||||
}
|
||||
.chat-composer-shell {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.12));
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
|
||||
background: var(--ds-composer-shell-bg, rgba(0, 0, 0, 0.28));
|
||||
padding: 6px 8px 6px 6px;
|
||||
}
|
||||
.chat-composer-shell--busy .chat-composer__field {
|
||||
|
|
@ -349,7 +415,7 @@
|
|||
max-width: 100%;
|
||||
padding: 2px 8px 2px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.12));
|
||||
font-size: 11px;
|
||||
}
|
||||
.chat-pending-row .muted {
|
||||
|
|
@ -373,7 +439,7 @@
|
|||
}
|
||||
.chat-pending-row .chat-pending-remove:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16));
|
||||
}
|
||||
.chat-composer-row {
|
||||
display: flex;
|
||||
|
|
@ -390,15 +456,15 @@
|
|||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--ds-text-muted, rgba(232, 232, 232, 0.75));
|
||||
color: var(--ds-text-muted, rgba(148, 163, 184, 0.85));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chat-composer-iconbtn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--ds-text, #e8e8e8);
|
||||
background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.12));
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
}
|
||||
.chat-composer-iconbtn:focus-visible {
|
||||
outline: 1px solid var(--chat-sess-border, rgba(255, 255, 255, 0.25));
|
||||
|
|
@ -677,22 +743,35 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<!-- Load markdown/mermaid helpers before chat.js (avoid raw ``` fences render). -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js" crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js" crossorigin="anonymous"></script>
|
||||
<script
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"
|
||||
crossorigin="anonymous"
|
||||
onload="(function(){try{ if (window.__oclawHydrateMermaidAll) window.__oclawHydrateMermaidAll(); }catch(e){}})()"
|
||||
></script>
|
||||
<!-- Cache-bust for desktop webview: avoid stale chat.js -->
|
||||
<!-- Local markdown helpers (vendored; no CDN). Load marked/purify before chat.js. -->
|
||||
<script defer src="/admin/assets/vendor/marked.min.js"></script>
|
||||
<script defer src="/admin/assets/vendor/purify.min.js"></script>
|
||||
<script defer src="/admin/assets/admin-theme.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="/admin/assets/chat.js?v=20260626-1"
|
||||
src="/admin/assets/chat.js?v=20260811-vendor"
|
||||
onerror="(function(){var s=document.getElementById('chat-boot-splash');if(s){s.querySelector('.chat-boot-splash__title span:last-child').textContent='无法加载 chat.js';s.querySelector('.chat-boot-splash__muted').textContent='请确认网关已启动且 /admin/assets/chat.js 可访问。';}})()"
|
||||
></script>
|
||||
<!-- Mermaid is larger; load after chat.js so login is not delayed by parse. -->
|
||||
<script
|
||||
defer
|
||||
src="/admin/assets/vendor/mermaid.min.js"
|
||||
onload="(function(){try{ if (window.__oclawHydrateMermaidAll) window.__oclawHydrateMermaidAll(); }catch(e){}})()"
|
||||
></script>
|
||||
<script>
|
||||
(function () {
|
||||
setTimeout(function () {
|
||||
var s = document.getElementById("chat-boot-splash");
|
||||
if (!s || !document.body.contains(s)) return;
|
||||
var title = s.querySelector(".chat-boot-splash__title span:last-child");
|
||||
var muted = s.querySelector(".chat-boot-splash__muted");
|
||||
if (title) title.textContent = "加载仍在进行…";
|
||||
if (muted) {
|
||||
muted.textContent = "若超过数秒仍无登录框,请确认网关已启动后刷新。桌面端可查看 desktop.log / backend.log。";
|
||||
}
|
||||
}, 4000);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="theme-ds-body chat-standalone-page">
|
||||
<div id="app">
|
||||
|
|
@ -700,10 +779,10 @@
|
|||
<div class="chat-boot-splash__card">
|
||||
<div class="chat-boot-splash__title">
|
||||
<span class="chat-boot-splash__spinner" aria-hidden="true"></span>
|
||||
<span>正在加载登录界面…</span>
|
||||
<span>正在加载…</span>
|
||||
</div>
|
||||
<div class="chat-boot-splash__muted">
|
||||
若长时间停留在此,请检查网络或稍后重试;桌面端可查看日志目录中的 desktop.log / backend.log。
|
||||
正在启动本地界面;若长时间停留,请确认网关已启动。桌面端可查看 desktop.log / backend.log。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ async function _buildAggregatedAssistantBubble(tsIso, items) {
|
|||
}
|
||||
const hasVisible =
|
||||
!!inner.querySelector(".chat-msg__md, .chat-msg__reasoning, .chat-msg__wiki") ||
|
||||
!!inner.querySelector(".chat-att-wrap, img.chat-att-img, a.chat-att-ref__link, .chat-att-chip") ||
|
||||
!!inner.querySelector(".chat-att-wrap, img.chat-att-img, a.chat-att-ref__link, a.chat-att-ref__action, .chat-att-chip") ||
|
||||
String(inner.textContent || "").trim().length > 0;
|
||||
if (!hasVisible) {
|
||||
inner.appendChild(el("div", { class: "muted", text: t("chat.tools.hidden") }));
|
||||
|
|
@ -1442,6 +1442,19 @@ function replaceSessionUrl(sessionId) {
|
|||
history.replaceState(null, "", path + q);
|
||||
}
|
||||
|
||||
function withTimeout(promise, ms, label) {
|
||||
let timer = null;
|
||||
const timeoutMs = Math.max(200, Number(ms) || 0);
|
||||
return Promise.race([
|
||||
Promise.resolve(promise),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(label || `timeout:${timeoutMs}`)), timeoutMs);
|
||||
}),
|
||||
]).finally(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
async function apiGet(path) {
|
||||
const token = localStorage.getItem(AUTH_TOKEN_KEY) || "";
|
||||
const headers = { accept: "application/json" };
|
||||
|
|
@ -1535,6 +1548,160 @@ function escapeHtml(s) {
|
|||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
/** Pretty-print channel session titles like `whatsapp|AI nms · Oliver · wa-default+86…@s.whatsapp.net`. */
|
||||
function formatChatSessionTitle(raw, peerNameHint) {
|
||||
const full = String(raw || "").trim();
|
||||
const badgeMap = {
|
||||
whatsapp: "WA",
|
||||
wechat: "WX",
|
||||
weixin: "WX",
|
||||
wecom: "WC",
|
||||
telegram: "TG",
|
||||
discord: "DC",
|
||||
slack: "SL",
|
||||
local: "Local",
|
||||
};
|
||||
if (!full) {
|
||||
return { channel: "local", badge: badgeMap.local, label: "—", sublabel: "", full: "" };
|
||||
}
|
||||
const m = full.match(/^(whatsapp|wechat|weixin|wecom|telegram|discord|slack)\|(.+)$/i);
|
||||
if (!m) {
|
||||
return { channel: "local", badge: badgeMap.local, label: full, sublabel: "", full };
|
||||
}
|
||||
const channel = String(m[1] || "").toLowerCase();
|
||||
const rest = String(m[2] || "").trim();
|
||||
const segments = rest
|
||||
.split(/\s*·\s*/)
|
||||
.map((s) => String(s || "").trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const isTech = (s) => /[+]/.test(s) || /@/.test(s) || /^wa-default/i.test(s);
|
||||
let techIdx = -1;
|
||||
for (let i = 0; i < segments.length; i += 1) {
|
||||
if (isTech(segments[i])) techIdx = i;
|
||||
}
|
||||
let labels = [];
|
||||
let tech = rest;
|
||||
if (techIdx >= 0) {
|
||||
labels = segments.slice(0, techIdx);
|
||||
tech = segments.slice(techIdx).join(" · ");
|
||||
} else if (segments.length > 1) {
|
||||
labels = segments.slice(0, -1);
|
||||
tech = segments[segments.length - 1] || "";
|
||||
}
|
||||
|
||||
// Legacy titles buried group name after the JID: account+jid@domain+GroupName
|
||||
let techBody = String(tech || "").replace(/^wa-default\+/i, "");
|
||||
const plusParts = techBody.split("+").map((s) => String(s || "").trim()).filter(Boolean);
|
||||
const idParts = [];
|
||||
const legacyNames = [];
|
||||
for (const p of plusParts) {
|
||||
if (p.includes("@") || /^[0-9]{8,}$/.test(p) || /^[0-9a-f]{16,}$/i.test(p)) idParts.push(p);
|
||||
else legacyNames.push(p);
|
||||
}
|
||||
for (let i = legacyNames.length - 1; i >= 0; i -= 1) {
|
||||
const n = legacyNames[i];
|
||||
if (n && !labels.includes(n)) labels.unshift(n); // group name first
|
||||
}
|
||||
let idRaw = idParts.join("+") || (legacyNames.length ? "" : techBody);
|
||||
let idShort = idRaw
|
||||
.replace(/@s\.whatsapp\.net$/i, "")
|
||||
.replace(/@g\.us$/i, "")
|
||||
.replace(/@lid$/i, "")
|
||||
.replace(/@im\.bot[^\s]*/i, "")
|
||||
.replace(/@[^\s]+$/i, "");
|
||||
if (/^[0-9a-f]{20,}$/i.test(idShort)) idShort = `${idShort.slice(0, 8)}…`;
|
||||
else if (idShort.length > 18) idShort = `${idShort.slice(0, 16)}…`;
|
||||
|
||||
const hint = String(peerNameHint || "").trim();
|
||||
if (hint && !labels.some((l) => l.toLowerCase() === hint.toLowerCase())) {
|
||||
// Prefer keeping server-provided group/nick labels; append contact hint when useful.
|
||||
if (!labels.length) labels.push(hint);
|
||||
else if (labels.length === 1 && labels[0] !== hint) labels.push(hint);
|
||||
}
|
||||
|
||||
const badge = badgeMap[channel] || channel.slice(0, 2).toUpperCase();
|
||||
let label = "";
|
||||
if (labels.length) {
|
||||
label = labels.join(" · ");
|
||||
// DM / unknown: single nick still benefits from a short id.
|
||||
if (labels.length === 1 && idShort && labels[0] === hint) {
|
||||
label = `${labels[0]} · ${idShort}`;
|
||||
}
|
||||
} else {
|
||||
label = idShort || full;
|
||||
}
|
||||
return {
|
||||
channel,
|
||||
badge,
|
||||
label,
|
||||
sublabel: "",
|
||||
full,
|
||||
idKey: idRaw || techBody,
|
||||
};
|
||||
}
|
||||
|
||||
function _peerNameLookupKeys(title, pretty) {
|
||||
const keys = [];
|
||||
const push = (v) => {
|
||||
const s = String(v || "").trim().toLowerCase();
|
||||
if (s) keys.push(s);
|
||||
};
|
||||
push(pretty && pretty.idKey);
|
||||
const raw = String(title || "");
|
||||
const m = raw.match(/^(whatsapp|wechat|weixin|wecom)\|(.+)$/i);
|
||||
if (m) {
|
||||
let body = String(m[2] || "");
|
||||
const segs = body.split(/\s*·\s*/).map((s) => String(s || "").trim()).filter(Boolean);
|
||||
let tech = body;
|
||||
for (let i = segs.length - 1; i >= 0; i -= 1) {
|
||||
if (/[+]/.test(segs[i]) || /@/.test(segs[i]) || /^wa-default/i.test(segs[i])) {
|
||||
tech = segs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(tech);
|
||||
push(tech.replace(/^wa-default\+/i, ""));
|
||||
const noAt = tech.replace(/@.*$/, "");
|
||||
push(noAt);
|
||||
push(noAt.replace(/^wa-default\+/i, ""));
|
||||
// Drop trailing +GroupName legacy suffix for contact id matching.
|
||||
const core = noAt.replace(/^wa-default\+/i, "").split("+")[0] || "";
|
||||
push(core);
|
||||
push(core.replace(/\D/g, ""));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
let _channelPeerNameMap = new Map();
|
||||
|
||||
function lookupChannelPeerName(title, pretty) {
|
||||
for (const k of _peerNameLookupKeys(title, pretty)) {
|
||||
const hit = _channelPeerNameMap.get(k);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function refreshChannelPeerNameMap() {
|
||||
const next = new Map();
|
||||
try {
|
||||
const r = await apiGet("/admin/api/whatsapp/access");
|
||||
const contacts = Array.isArray(r && r.contacts) ? r.contacts : [];
|
||||
for (const c of contacts) {
|
||||
const name = String((c && c.push_name) || "").trim();
|
||||
if (!name) continue;
|
||||
const eid = String((c && c.external_user_id) || "").trim();
|
||||
const phone = String((c && c.phone) || "").trim();
|
||||
for (const raw of [eid, phone, eid.replace(/@.*$/, ""), phone.replace(/\D/g, "")]) {
|
||||
const k = String(raw || "").trim().toLowerCase();
|
||||
if (k) next.set(k, name);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
_channelPeerNameMap = next;
|
||||
}
|
||||
|
||||
let _chatLightboxKeyHandler = null;
|
||||
let _chatLightboxPrevOverflow = "";
|
||||
|
||||
|
|
@ -2677,15 +2844,16 @@ async function renderAttachmentsEl(raw) {
|
|||
const bytes = Number(att.bytes || 0);
|
||||
const sizeLabel = bytes > 0 ? `${Math.round((bytes / 1024) * 10) / 10} KB` : "";
|
||||
const card = el("div", { class: "chat-att-ref" });
|
||||
if (aid) card.title = `id: ${aid}`;
|
||||
card.appendChild(el("div", { class: "chat-att-ref__name", text: name }));
|
||||
card.appendChild(el("div", { class: "chat-att-ref__meta", text: `${typ} · ${mime}${sizeLabel ? ` · ${sizeLabel}` : ""}` }));
|
||||
if (aid) card.appendChild(el("div", { class: "chat-att-ref__meta", text: `id: ${aid.slice(0, 16)}...` }));
|
||||
const actions = el("div", { class: "chat-att-ref__actions" });
|
||||
if (aid) {
|
||||
const url = await fetchAttachmentBlobUrl(aid);
|
||||
if (url) {
|
||||
card.appendChild(
|
||||
actions.appendChild(
|
||||
el("a", {
|
||||
class: "chat-att-ref__link",
|
||||
class: "chat-att-ref__action",
|
||||
href: url,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
|
|
@ -2715,9 +2883,9 @@ async function renderAttachmentsEl(raw) {
|
|||
src: remote,
|
||||
}),
|
||||
);
|
||||
card.appendChild(
|
||||
actions.appendChild(
|
||||
el("a", {
|
||||
class: "chat-att-ref__link",
|
||||
class: "chat-att-ref__action",
|
||||
href: remote,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
|
|
@ -2730,7 +2898,7 @@ async function renderAttachmentsEl(raw) {
|
|||
if (canPreviewText) {
|
||||
const preview = el("button", {
|
||||
type: "button",
|
||||
class: "chat-att-ref__btn",
|
||||
class: "chat-att-ref__action",
|
||||
text: t("chat.attachment.preview"),
|
||||
});
|
||||
const pre = el("pre", { class: "chat-att-ref__preview" });
|
||||
|
|
@ -2753,8 +2921,11 @@ async function renderAttachmentsEl(raw) {
|
|||
preview.textContent = t("chat.attachment.preview");
|
||||
}
|
||||
});
|
||||
card.appendChild(preview);
|
||||
actions.appendChild(preview);
|
||||
card.appendChild(actions);
|
||||
card.appendChild(pre);
|
||||
} else if (actions.childNodes.length) {
|
||||
card.appendChild(actions);
|
||||
}
|
||||
return card;
|
||||
};
|
||||
|
|
@ -2804,7 +2975,7 @@ async function renderAttachmentsEl(raw) {
|
|||
if (!parent) return;
|
||||
parent.replaceChild(
|
||||
el("a", {
|
||||
class: "chat-att-ref__link",
|
||||
class: "chat-att-ref__action",
|
||||
href: src,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
|
|
@ -2969,11 +3140,126 @@ function syncAuthUserLabel() {
|
|||
.trim()
|
||||
.toLowerCase() === "administrator";
|
||||
if (!name) return;
|
||||
const openUserMenu = (anchorEl) => {
|
||||
clearChatPageBlockers();
|
||||
const items = [
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "profile",
|
||||
text: t("chat.myProfile"),
|
||||
}),
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "jobs",
|
||||
text: t("chat.jobs"),
|
||||
}),
|
||||
];
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("div", { class: "muted u-menu-label", text: t("theme.label") }),
|
||||
);
|
||||
const themeSelMenu = el("select", {
|
||||
class: "input u-menu-select",
|
||||
});
|
||||
try {
|
||||
(window.OclawAdminTheme && window.OclawAdminTheme.THEMES ? window.OclawAdminTheme.THEMES : ["netx"]).forEach((tid) => {
|
||||
themeSelMenu.appendChild(el("option", { value: tid, text: t(`theme.${tid}`) }));
|
||||
});
|
||||
themeSelMenu.value = window.OclawAdminTheme ? window.OclawAdminTheme.currentAdminTheme() : "netx";
|
||||
} catch (_) {}
|
||||
themeSelMenu.addEventListener("change", () => {
|
||||
try {
|
||||
if (window.OclawAdminTheme) window.OclawAdminTheme.persistAdminTheme(themeSelMenu.value);
|
||||
} catch (_) {}
|
||||
});
|
||||
items.push(themeSelMenu);
|
||||
const bridge = window.__chatUserMenuPrefs;
|
||||
if (bridge && typeof bridge === "object") {
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(el("div", { class: "muted u-menu-label", text: t("chat.modeLabel") }));
|
||||
const modeSel = el("select", { class: "input u-menu-select" });
|
||||
try {
|
||||
const rows = Array.isArray(bridge.getModeOptions && bridge.getModeOptions()) ? bridge.getModeOptions() : [];
|
||||
rows.forEach((r) => modeSel.appendChild(el("option", { value: String(r.value || ""), text: String(r.label || r.value || "") })));
|
||||
modeSel.value = String((bridge.getModeValue && bridge.getModeValue()) || "");
|
||||
} catch (_) {}
|
||||
modeSel.addEventListener("change", async () => {
|
||||
const v = modeSel.value;
|
||||
try {
|
||||
modeSel.disabled = true;
|
||||
if (bridge.setModeValue) await bridge.setModeValue(v);
|
||||
} catch (_) {
|
||||
// errors are surfaced by saveUserGlobalModePreference()
|
||||
} finally {
|
||||
modeSel.disabled = false;
|
||||
}
|
||||
});
|
||||
items.push(modeSel);
|
||||
const reasonWrap = el("label", { class: "switch-wrap u-switch-menu" }, [
|
||||
el("input", { type: "checkbox", class: "switch-input" }),
|
||||
el("span", { class: "switch-slider" }),
|
||||
el("span", { class: "muted", text: t("chat.tools") }),
|
||||
]);
|
||||
const reasonCb = reasonWrap.querySelector("input.switch-input");
|
||||
try {
|
||||
reasonCb.checked = !!(bridge.getReasoningVisible && bridge.getReasoningVisible());
|
||||
} catch (_) {}
|
||||
reasonCb.addEventListener("change", () => {
|
||||
try {
|
||||
Promise.resolve(bridge.setReasoningVisible && bridge.setReasoningVisible(!!reasonCb.checked)).catch(() => {});
|
||||
} catch (_) {}
|
||||
});
|
||||
items.push(reasonWrap);
|
||||
}
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "lang",
|
||||
text: t("lang.switch"),
|
||||
}),
|
||||
);
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "logout",
|
||||
text: t("auth.logout"),
|
||||
}),
|
||||
);
|
||||
const menu = el("div", {
|
||||
class: "chat-sess-menu-pop u-pop-menu",
|
||||
}, items);
|
||||
// Inline position/z-index: chat.html used to override class z-index below the scrim.
|
||||
menu.style.position = "fixed";
|
||||
menu.style.zIndex = "300";
|
||||
const rect = (anchorEl || moreBtn).getBoundingClientRect();
|
||||
attachChatMenuDismiss(menu);
|
||||
const mrect = menu.getBoundingClientRect();
|
||||
const pad = 8;
|
||||
let left = rect.left;
|
||||
let top = rect.bottom + 4;
|
||||
if (top + mrect.height > window.innerHeight - pad) {
|
||||
top = rect.top - 4 - mrect.height;
|
||||
}
|
||||
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
|
||||
top = Math.max(pad, Math.min(top, window.innerHeight - pad - mrect.height));
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
};
|
||||
const nameBtn = el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-btn",
|
||||
text: name,
|
||||
title: name,
|
||||
onclick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
openUserMenu(ev.currentTarget);
|
||||
},
|
||||
});
|
||||
const moreBtn = el("button", {
|
||||
type: "button",
|
||||
|
|
@ -2982,112 +3268,7 @@ function syncAuthUserLabel() {
|
|||
title: t("chat.sessionMenu"),
|
||||
onclick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
clearChatPageBlockers();
|
||||
const items = [
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "profile",
|
||||
text: t("chat.myProfile"),
|
||||
}),
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "jobs",
|
||||
text: t("chat.jobs"),
|
||||
}),
|
||||
];
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("div", { class: "muted u-menu-label", text: t("theme.label") }),
|
||||
);
|
||||
const themeSelMenu = el("select", {
|
||||
class: "input u-menu-select",
|
||||
});
|
||||
try {
|
||||
(window.OclawAdminTheme && window.OclawAdminTheme.THEMES ? window.OclawAdminTheme.THEMES : ["netx"]).forEach((tid) => {
|
||||
themeSelMenu.appendChild(el("option", { value: tid, text: t(`theme.${tid}`) }));
|
||||
});
|
||||
themeSelMenu.value = window.OclawAdminTheme ? window.OclawAdminTheme.currentAdminTheme() : "netx";
|
||||
} catch (_) {}
|
||||
themeSelMenu.addEventListener("change", () => {
|
||||
try {
|
||||
if (window.OclawAdminTheme) window.OclawAdminTheme.persistAdminTheme(themeSelMenu.value);
|
||||
} catch (_) {}
|
||||
});
|
||||
items.push(themeSelMenu);
|
||||
const bridge = window.__chatUserMenuPrefs;
|
||||
if (bridge && typeof bridge === "object") {
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(el("div", { class: "muted u-menu-label", text: t("chat.modeLabel") }));
|
||||
const modeSel = el("select", { class: "input u-menu-select" });
|
||||
try {
|
||||
const rows = Array.isArray(bridge.getModeOptions && bridge.getModeOptions()) ? bridge.getModeOptions() : [];
|
||||
rows.forEach((r) => modeSel.appendChild(el("option", { value: String(r.value || ""), text: String(r.label || r.value || "") })));
|
||||
modeSel.value = String((bridge.getModeValue && bridge.getModeValue()) || "");
|
||||
} catch (_) {}
|
||||
modeSel.addEventListener("change", async () => {
|
||||
const v = modeSel.value;
|
||||
try {
|
||||
modeSel.disabled = true;
|
||||
if (bridge.setModeValue) await bridge.setModeValue(v);
|
||||
} catch (_) {
|
||||
// errors are surfaced by saveUserGlobalModePreference()
|
||||
} finally {
|
||||
modeSel.disabled = false;
|
||||
}
|
||||
});
|
||||
items.push(modeSel);
|
||||
const reasonWrap = el("label", { class: "switch-wrap u-switch-menu" }, [
|
||||
el("input", { type: "checkbox", class: "switch-input" }),
|
||||
el("span", { class: "switch-slider" }),
|
||||
el("span", { class: "muted", text: t("chat.tools") }),
|
||||
]);
|
||||
const reasonCb = reasonWrap.querySelector("input.switch-input");
|
||||
try {
|
||||
reasonCb.checked = !!(bridge.getReasoningVisible && bridge.getReasoningVisible());
|
||||
} catch (_) {}
|
||||
reasonCb.addEventListener("change", () => {
|
||||
try {
|
||||
Promise.resolve(bridge.setReasoningVisible && bridge.setReasoningVisible(!!reasonCb.checked)).catch(() => {});
|
||||
} catch (_) {}
|
||||
});
|
||||
items.push(reasonWrap);
|
||||
}
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "lang",
|
||||
text: t("lang.switch"),
|
||||
}),
|
||||
);
|
||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||
items.push(
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-menu-item",
|
||||
"data-menu-action": "logout",
|
||||
text: t("auth.logout"),
|
||||
}),
|
||||
);
|
||||
const menu = el("div", {
|
||||
class: "chat-sess-menu-pop u-pop-menu",
|
||||
}, items);
|
||||
const rect = moreBtn.getBoundingClientRect();
|
||||
attachChatMenuDismiss(menu);
|
||||
const mrect = menu.getBoundingClientRect();
|
||||
const pad = 8;
|
||||
let left = rect.left;
|
||||
let top = rect.bottom + 4;
|
||||
if (top + mrect.height > window.innerHeight - pad) {
|
||||
top = rect.top - 4 - mrect.height;
|
||||
}
|
||||
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
|
||||
top = Math.max(pad, Math.min(top, window.innerHeight - pad - mrect.height));
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
openUserMenu(ev.currentTarget);
|
||||
},
|
||||
});
|
||||
const row = el("div", { class: "chat-user-row" }, [nameBtn, moreBtn]);
|
||||
|
|
@ -3293,7 +3474,7 @@ async function renderChatUi() {
|
|||
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
|
||||
if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
const modelSelect = el("select", {
|
||||
class: "input u-select-mode",
|
||||
class: "chat-composer-chip chat-composer-chip--select",
|
||||
title: t("chat.activeModelLabel"),
|
||||
});
|
||||
let modelSelectNameToId = new Map();
|
||||
|
|
@ -3566,41 +3747,42 @@ async function renderChatUi() {
|
|||
};
|
||||
await refreshActiveModelText();
|
||||
refreshExecUi();
|
||||
const composerMetaBar = el("div", { class: "row u-gap-8 u-composer-meta" }, [
|
||||
const compressBtn = el("button", {
|
||||
type: "button",
|
||||
class: "chat-composer-chip",
|
||||
text: t("chat.compressHistory"),
|
||||
onclick: async () => {
|
||||
const sid = String(activeId || "");
|
||||
if (!sid) return;
|
||||
if (!(await confirmChatAction(t("chat.compressHistoryPrompt")))) return;
|
||||
try {
|
||||
const resp = await apiPost(`/admin/api/chat/sessions/${encodeURIComponent(sid)}/compress-history`, {});
|
||||
const r = resp && resp.result ? resp.result : {};
|
||||
if (!resp || !resp.ok) {
|
||||
const err = String((resp && (resp.error || resp.detail)) || "failed");
|
||||
showToast(t("chat.compressHistoryFail", { error: err.slice(0, 180) }), { kind: "error", ttlMs: 6500 });
|
||||
return;
|
||||
}
|
||||
showToast(
|
||||
t("chat.compressHistoryOk", {
|
||||
scanned: String(Number(r.scanned_tool_messages || 0)),
|
||||
rewritten: String(Number(r.rewritten_all_tool_messages || 0)),
|
||||
compacted: String(Number(r.compacted_tool_messages || 0)),
|
||||
skipped: String(Number(r.skipped_already_guarded || 0)),
|
||||
}),
|
||||
{ kind: "info", ttlMs: 6500 },
|
||||
);
|
||||
// Reload messages so UI reflects compacted history.
|
||||
loadMessagesForActive().catch(() => {});
|
||||
} catch (e) {
|
||||
showToast(t("chat.compressHistoryFail", { error: String(e || "failed").slice(0, 180) }), { kind: "error", ttlMs: 6500 });
|
||||
}
|
||||
},
|
||||
});
|
||||
const composerMetaBar = el("div", { class: "chat-composer-meta" }, [
|
||||
execSelectWrap,
|
||||
modelSelect,
|
||||
el("button", {
|
||||
type: "button",
|
||||
class: "btn u-btn-compact",
|
||||
text: t("chat.compressHistory"),
|
||||
onclick: async () => {
|
||||
const sid = String(activeId || "");
|
||||
if (!sid) return;
|
||||
if (!(await confirmChatAction(t("chat.compressHistoryPrompt")))) return;
|
||||
try {
|
||||
const resp = await apiPost(`/admin/api/chat/sessions/${encodeURIComponent(sid)}/compress-history`, {});
|
||||
const r = resp && resp.result ? resp.result : {};
|
||||
if (!resp || !resp.ok) {
|
||||
const err = String((resp && (resp.error || resp.detail)) || "failed");
|
||||
showToast(t("chat.compressHistoryFail", { error: err.slice(0, 180) }), { kind: "error", ttlMs: 6500 });
|
||||
return;
|
||||
}
|
||||
showToast(
|
||||
t("chat.compressHistoryOk", {
|
||||
scanned: String(Number(r.scanned_tool_messages || 0)),
|
||||
rewritten: String(Number(r.rewritten_all_tool_messages || 0)),
|
||||
compacted: String(Number(r.compacted_tool_messages || 0)),
|
||||
skipped: String(Number(r.skipped_already_guarded || 0)),
|
||||
}),
|
||||
{ kind: "info", ttlMs: 6500 },
|
||||
);
|
||||
// Reload messages so UI reflects compacted history.
|
||||
loadMessagesForActive().catch(() => {});
|
||||
} catch (e) {
|
||||
showToast(t("chat.compressHistoryFail", { error: String(e || "failed").slice(0, 180) }), { kind: "error", ttlMs: 6500 });
|
||||
}
|
||||
},
|
||||
}),
|
||||
compressBtn,
|
||||
]);
|
||||
|
||||
const fitComposerTextarea = () => {
|
||||
|
|
@ -3958,20 +4140,44 @@ async function renderChatUi() {
|
|||
for (const s of sessions) {
|
||||
const sid = String(s.id || "");
|
||||
const title = String(s.title || sid || "");
|
||||
const peerHint = String(s.peer_name || s.peer_display_name || "").trim();
|
||||
let pretty = formatChatSessionTitle(title, peerHint);
|
||||
if (!pretty.sublabel && pretty.channel) {
|
||||
const looked = lookupChannelPeerName(title, pretty);
|
||||
if (looked) pretty = formatChatSessionTitle(title, looked);
|
||||
}
|
||||
const row = el("div", { class: "chat-sess-row" + (idsMatch(activeId, sid) ? " chat-sess-row--active" : "") });
|
||||
const btn = el("button", {
|
||||
class: "chat-sess-btn" + (idsMatch(activeId, sid) ? " chat-sess-btn--active" : ""),
|
||||
text: title,
|
||||
onclick: () => {
|
||||
activeId = sid;
|
||||
replaceSessionUrl(sid);
|
||||
paintSessions();
|
||||
Promise.all([loadMessagesForActive(), loadSessionModePreference()]).catch((e) => {
|
||||
statusBar.textContent = `${t("chat.error")}: ${String(e)}`;
|
||||
});
|
||||
startWikiPoller();
|
||||
const btnChildren = [];
|
||||
if (pretty.badge) {
|
||||
btnChildren.push(
|
||||
el("span", {
|
||||
class: `chat-sess-badge chat-sess-badge--${pretty.channel || "other"}`,
|
||||
text: pretty.badge,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const labelKids = [el("span", { class: "chat-sess-btn__name", text: pretty.label })];
|
||||
if (pretty.sublabel) {
|
||||
labelKids.push(el("span", { class: "chat-sess-btn__id", text: pretty.sublabel }));
|
||||
}
|
||||
btnChildren.push(el("span", { class: "chat-sess-btn__label" }, labelKids));
|
||||
const btn = el(
|
||||
"button",
|
||||
{
|
||||
class: "chat-sess-btn" + (idsMatch(activeId, sid) ? " chat-sess-btn--active" : ""),
|
||||
title: pretty.full || title,
|
||||
onclick: () => {
|
||||
activeId = sid;
|
||||
replaceSessionUrl(sid);
|
||||
paintSessions();
|
||||
Promise.all([loadMessagesForActive(), loadSessionModePreference()]).catch((e) => {
|
||||
statusBar.textContent = `${t("chat.error")}: ${String(e)}`;
|
||||
});
|
||||
startWikiPoller();
|
||||
},
|
||||
},
|
||||
});
|
||||
btnChildren,
|
||||
);
|
||||
const more = el("button", {
|
||||
type: "button",
|
||||
class: "chat-sess-more" + (idsMatch(activeId, sid) ? " chat-sess-more--active" : ""),
|
||||
|
|
@ -4106,6 +4312,7 @@ async function renderChatUi() {
|
|||
};
|
||||
|
||||
const reloadSessionsOnly = async () => {
|
||||
await refreshChannelPeerNameMap().catch(() => {});
|
||||
const resp = await apiGet(`/admin/api/chat/sessions?limit=${PAGE_SIZE}&offset=0`);
|
||||
sessions = Array.isArray(resp.sessions) ? resp.sessions : [];
|
||||
sessionTotal = intOr(resp.total, sessions.length);
|
||||
|
|
@ -4114,6 +4321,7 @@ async function renderChatUi() {
|
|||
|
||||
const refreshSessions = async () => {
|
||||
statusBar.textContent = t("chat.loading");
|
||||
await refreshChannelPeerNameMap().catch(() => {});
|
||||
const resp = await apiGet(`/admin/api/chat/sessions?limit=${PAGE_SIZE}&offset=0`);
|
||||
sessions = Array.isArray(resp.sessions) ? resp.sessions : [];
|
||||
sessionTotal = intOr(resp.total, sessions.length);
|
||||
|
|
@ -5709,15 +5917,19 @@ async function boot() {
|
|||
localStorage.removeItem(AUTH_SESSION_KEY);
|
||||
authSession = null;
|
||||
try {
|
||||
await apiPost("/admin/api/auth/bootstrap", {});
|
||||
await withTimeout(apiPost("/admin/api/auth/bootstrap", {}), 2500, "auth_bootstrap_timeout");
|
||||
} catch (_) {}
|
||||
mount(await renderLogin());
|
||||
applyI18nStatic();
|
||||
syncAuthUserLabel();
|
||||
return;
|
||||
}
|
||||
await syncLangFromServer();
|
||||
await loadMeProfile();
|
||||
try {
|
||||
await withTimeout(syncLangFromServer(), 2500, "ui_lang_timeout");
|
||||
} catch (_) {}
|
||||
try {
|
||||
await withTimeout(loadMeProfile(), 2500, "me_profile_timeout");
|
||||
} catch (_) {}
|
||||
try {
|
||||
mount(await renderChatUi());
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
<div id="topTitle" class="topbar__title">Runtime</div>
|
||||
<div class="topbar__actions">
|
||||
<span id="authUser" class="muted"></span>
|
||||
<label class="admin-theme-wrap u-inline-flex u-row-center">
|
||||
<label class="admin-theme-wrap">
|
||||
<span class="muted u-fs-12" data-i18n="theme.label">配色</span>
|
||||
<select id="adminThemeSelect" class="input input--compact u-theme-select" aria-label="theme">
|
||||
<option value="netx" data-i18n="theme.netx">NetX</option>
|
||||
|
|
@ -108,10 +108,10 @@
|
|||
<option value="light" data-i18n="theme.light">Light</option>
|
||||
</select>
|
||||
</label>
|
||||
<a id="btnBackChat" class="btn" data-i18n="nav.chat" href="chat">对话</a>
|
||||
<button id="btnLogout" class="btn" data-i18n="auth.logout">退出登录</button>
|
||||
<button id="btnLang" class="btn">中文</button>
|
||||
<button id="btnRefresh" class="btn" data-i18n="action.refresh">刷新</button>
|
||||
<a id="btnBackChat" class="btn btn--primary" data-i18n="nav.chat" href="chat">对话</a>
|
||||
<button id="btnLogout" class="btn btn--ghost" type="button" data-i18n="auth.logout">退出登录</button>
|
||||
<button id="btnLang" class="btn btn--ghost" type="button">中文</button>
|
||||
<button id="btnRefresh" class="btn btn--ghost" type="button" data-i18n="action.refresh">刷新</button>
|
||||
</div>
|
||||
</header>
|
||||
<section id="content" class="content">
|
||||
|
|
|
|||
|
|
@ -110,23 +110,32 @@ body {
|
|||
}
|
||||
.nav__groupTitle {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
padding: 6px 12px 2px;
|
||||
padding: 10px 12px 4px;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.nav__item {
|
||||
text-decoration: none;
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
padding: 10px 12px;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
.nav__item:hover {
|
||||
background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.1));
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
}
|
||||
.nav__item:hover { background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.1)); }
|
||||
.nav__item--active {
|
||||
background: var(--ds-accent-soft, rgba(59, 130, 246, 0.22));
|
||||
border-color: var(--ds-accent-border, rgba(59, 130, 246, 0.4));
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
box-shadow: inset 3px 0 0 var(--ds-accent, #3b82f6);
|
||||
}
|
||||
.sidebar__footer {
|
||||
margin-top: auto;
|
||||
|
|
@ -161,43 +170,144 @@ body {
|
|||
letter-spacing: 0.02em;
|
||||
}
|
||||
.topbar .row { align-items: center; gap: 8px; }
|
||||
.topbar .input--compact { height: 30px; padding: 4px 8px; font-size: 12px; }
|
||||
.topbar .btn { height: 30px; padding: 4px 10px; font-size: 12px; }
|
||||
.topbar__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.topbar__actions > #authUser {
|
||||
margin-right: 4px;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.16));
|
||||
font-size: 12px;
|
||||
}
|
||||
.admin-theme-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 4px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
|
||||
}
|
||||
.admin-theme-wrap > .muted {
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Shared chrome size: buttons + theme select must match in the 48px topbar. */
|
||||
.topbar .btn,
|
||||
.topbar a.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border-radius: 7px;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
transform: none;
|
||||
}
|
||||
.topbar .btn--primary {
|
||||
box-shadow: none;
|
||||
}
|
||||
.topbar .btn--ghost {
|
||||
background: transparent;
|
||||
}
|
||||
.topbar .input,
|
||||
.topbar .input--compact,
|
||||
.topbar select.input {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
padding: 0 28px 0 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border-radius: 7px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.u-theme-select {
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
width: auto;
|
||||
}
|
||||
.admin-theme-wrap { margin-right: 4px; }
|
||||
.u-theme-select { min-width: 132px; max-width: 200px; padding: 6px 8px; }
|
||||
.btn {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.25));
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
padding: 10px 14px;
|
||||
min-height: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.btn:hover { background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16)); }
|
||||
.btn:active { transform: translateY(0.5px); }
|
||||
.btn--primary {
|
||||
background: var(--ds-accent-soft, rgba(59, 130, 246, 0.25));
|
||||
border-color: var(--ds-accent-border, rgba(59, 130, 246, 0.4));
|
||||
background: var(--ds-accent, #3b82f6);
|
||||
border-color: var(--ds-accent, #3b82f6);
|
||||
color: #f8fafc;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.12) inset;
|
||||
}
|
||||
.btn--primary:hover {
|
||||
background: var(--ds-accent-deep, #2563eb);
|
||||
border-color: var(--ds-accent-deep, #2563eb);
|
||||
}
|
||||
.btn--danger {
|
||||
background: rgba(239, 68, 68, 0.16);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #fecaca;
|
||||
}
|
||||
.btn--danger:hover {
|
||||
background: rgba(239, 68, 68, 0.24);
|
||||
border-color: rgba(239, 68, 68, 0.55);
|
||||
}
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
border-color: var(--ds-border, rgba(148, 163, 184, 0.22));
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
}
|
||||
.btn--ghost:hover {
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
background: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.1));
|
||||
}
|
||||
.btn--danger { background: rgba(239, 68, 68, 0.18); border-color: rgba(239, 68, 68, 0.35); }
|
||||
.content {
|
||||
padding: 18px 20px 28px;
|
||||
padding: 20px 22px 32px;
|
||||
overflow: auto;
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
background:
|
||||
radial-gradient(1200px 480px at 12% -10%, rgba(37, 99, 235, 0.14), transparent 55%),
|
||||
radial-gradient(900px 420px at 88% 0%, rgba(14, 165, 233, 0.08), transparent 50%),
|
||||
radial-gradient(1200px 480px at 12% -10%, rgba(37, 99, 235, 0.12), transparent 55%),
|
||||
radial-gradient(900px 420px at 88% 0%, rgba(14, 165, 233, 0.07), transparent 50%),
|
||||
var(--ds-bg, #070b14);
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
max-width: 1280px;
|
||||
}
|
||||
.page-shell__header {
|
||||
display: flex;
|
||||
|
|
@ -246,7 +356,7 @@ body {
|
|||
|
||||
.page-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
}
|
||||
.page-grid--single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
|
@ -254,6 +364,10 @@ body {
|
|||
.page-grid--two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.page-grid > .card {
|
||||
margin-bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.section-card {
|
||||
margin-bottom: 0;
|
||||
scroll-margin-top: 76px;
|
||||
|
|
@ -274,12 +388,176 @@ body {
|
|||
.card {
|
||||
background: var(--ds-panel, rgba(15, 23, 42, 0.92));
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 8px 24px rgba(2, 6, 23, 0.18);
|
||||
}
|
||||
.card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.card__headline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.card__title {
|
||||
font-weight: 650;
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
}
|
||||
.card__head .card__title { margin-bottom: 0; }
|
||||
.card__subtitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
max-width: 72ch;
|
||||
}
|
||||
.card__actions {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.card__hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
}
|
||||
.card__title { font-weight: 650; margin-bottom: 10px; }
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.form-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px 14px;
|
||||
}
|
||||
.form-inline__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1 1 160px;
|
||||
min-width: 140px;
|
||||
max-width: 280px;
|
||||
}
|
||||
.form-inline__field > label,
|
||||
.form-inline__label {
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
}
|
||||
.form-inline__field .input,
|
||||
.form-inline__field select.input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-inline__field--wide {
|
||||
max-width: none;
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
.form-inline__actions {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.meta-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.16));
|
||||
}
|
||||
.pid-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.pid-cell__id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.pid-cell__tag {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ds-accent-link, #60a5fa);
|
||||
opacity: 0.9;
|
||||
}
|
||||
.form-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 14px;
|
||||
}
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.form-field__label {
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
}
|
||||
.form-field .input,
|
||||
.form-field select.input,
|
||||
.form-field textarea.input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
.section-card .row {
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
|
|
@ -349,13 +627,66 @@ body {
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input {
|
||||
background: var(--ds-surface, #0f172a);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.25));
|
||||
.input,
|
||||
select.input,
|
||||
textarea.input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--ds-surface-2, #152033);
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.28));
|
||||
color: var(--ds-text, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
padding: 10px 12px;
|
||||
min-height: 40px;
|
||||
min-width: 240px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
textarea.input {
|
||||
min-height: 108px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
select.input {
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--ds-text-muted, #94a3b8) 50%),
|
||||
linear-gradient(135deg, var(--ds-text-muted, #94a3b8) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 16px) calc(50% - 2px),
|
||||
calc(100% - 11px) calc(50% - 2px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input::placeholder,
|
||||
textarea.input::placeholder {
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.input:hover,
|
||||
select.input:hover,
|
||||
textarea.input:hover {
|
||||
border-color: var(--ds-accent-border, rgba(59, 130, 246, 0.4));
|
||||
}
|
||||
.input:focus,
|
||||
select.input:focus,
|
||||
textarea.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--ds-accent-border-strong, rgba(59, 130, 246, 0.55));
|
||||
box-shadow: 0 0 0 3px var(--ds-accent-soft, rgba(59, 130, 246, 0.22));
|
||||
background: var(--ds-surface, #0f172a);
|
||||
}
|
||||
.input:disabled,
|
||||
select.input:disabled,
|
||||
textarea.input:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Unified switch style for admin/chat checkboxes */
|
||||
|
|
@ -405,7 +736,9 @@ input[type="checkbox"]:checked::before,
|
|||
}
|
||||
.input--compact {
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.input--readonly {
|
||||
max-width: min(100%, 720px);
|
||||
|
|
@ -419,17 +752,32 @@ input[type="checkbox"]:checked::before,
|
|||
}
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td {
|
||||
border-bottom: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--ds-border, rgba(148, 163, 184, 0.14));
|
||||
padding: 11px 12px;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table th {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ds-text-muted, #94a3b8);
|
||||
background: rgba(148, 163, 184, 0.05);
|
||||
}
|
||||
.table tbody tr:hover td {
|
||||
background: rgba(148, 163, 184, 0.04);
|
||||
}
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.12));
|
||||
border-radius: 8px;
|
||||
background: var(--ds-panel, rgba(15, 23, 42, 0.65));
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.14));
|
||||
border-radius: 10px;
|
||||
background: rgba(2, 6, 23, 0.22);
|
||||
}
|
||||
.table--compact { table-layout: fixed; min-width: 860px; }
|
||||
.table--compact th, .table--compact td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
|
@ -615,6 +963,19 @@ body.col-resize-active {
|
|||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 34px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.table .input,
|
||||
.table select.input {
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.btn--small {
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cell-copyable { cursor: copy; position: relative; }
|
||||
.cell-copyable:hover { background: var(--ds-accent-soft, rgba(59, 130, 246, 0.12)); }
|
||||
|
|
@ -645,15 +1006,28 @@ body.col-resize-active {
|
|||
}
|
||||
.plugins-pager .btn.btn--small { min-width: 72px; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.25));
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.badge--ok {
|
||||
border-color: rgba(34, 197, 94, 0.45);
|
||||
background: rgba(34, 197, 94, 0.14);
|
||||
color: #86efac;
|
||||
}
|
||||
.badge--bad {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
background: rgba(239, 68, 68, 0.14);
|
||||
color: #fca5a5;
|
||||
}
|
||||
.badge--ok { border-color: rgba(34, 197, 94, 0.4); background: rgba(34, 197, 94, 0.12); }
|
||||
.badge--bad { border-color: rgba(239, 68, 68, 0.45); background: rgba(239, 68, 68, 0.12); }
|
||||
.badge--mode-restricted { border-color: rgba(250, 204, 21, 0.45); background: rgba(250, 204, 21, 0.12); }
|
||||
.badge--mode-unrestricted {
|
||||
border-color: var(--ds-accent-border, rgba(59, 130, 246, 0.45));
|
||||
|
|
@ -665,15 +1039,54 @@ body.col-resize-active {
|
|||
font-size: 12px;
|
||||
}
|
||||
.alert {
|
||||
margin: 8px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.25));
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.alert__title {
|
||||
font-weight: 650;
|
||||
font-size: 13px;
|
||||
}
|
||||
.alert__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.alert--critical {
|
||||
border-color: rgba(239, 68, 68, 0.55);
|
||||
background: linear-gradient(180deg, rgba(239, 68, 68, 0.16), rgba(239, 68, 68, 0.08));
|
||||
color: #fecaca;
|
||||
}
|
||||
.alert--warning {
|
||||
border-color: rgba(250, 204, 21, 0.45);
|
||||
background: linear-gradient(180deg, rgba(250, 204, 21, 0.14), rgba(250, 204, 21, 0.06));
|
||||
color: #fde68a;
|
||||
}
|
||||
.alert--ok {
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
.alert-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: inherit;
|
||||
opacity: 0.92;
|
||||
font-size: 12px;
|
||||
}
|
||||
.alert--critical { border-color: rgba(239, 68, 68, 0.6); background: rgba(239, 68, 68, 0.12); color: #fecaca; }
|
||||
.alert-list { margin: 8px 0 0; padding-left: 18px; color: var(--ds-text-muted, #94a3b8); font-size: 13px; }
|
||||
.alert-list li { margin: 4px 0; }
|
||||
.alert--critical .alert-list,
|
||||
.alert--warning .alert-list {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Light theme: prefer light color-scheme for native controls */
|
||||
body.theme-ds-body[data-admin-theme="light"] {
|
||||
|
|
@ -691,7 +1104,8 @@ body.theme-ds-body[data-admin-theme="light"] .content {
|
|||
.u-mb-6 { margin: 0 0 6px 0; }
|
||||
.u-h-8 { height: 8px; }
|
||||
.u-h-10 { height: 10px; }
|
||||
.u-hidden { display: none !important; }
|
||||
/* No !important: many modals/panels toggle via element.style.display. */
|
||||
.u-hidden { display: none; }
|
||||
.u-drag { cursor: pointer; user-select: none; }
|
||||
.u-wrap-lh { white-space: normal; line-height: 1.5; }
|
||||
.u-muted-block { margin-top: 6px; line-height: 1.45; }
|
||||
|
|
@ -713,7 +1127,7 @@ body.theme-ds-body[data-admin-theme="light"] .content {
|
|||
.u-w-12p { width: 12%; }
|
||||
.u-w-full-mt-8 { width: 100%; margin-top: 8px; }
|
||||
.u-w-full-mt-10 { width: 100%; margin-top: 10px; }
|
||||
.u-overlay-fixed { position: fixed; z-index: 250; }
|
||||
.u-overlay-fixed { position: fixed; z-index: 300; }
|
||||
.u-overlay-scrim { position: fixed; inset: 0; z-index: 250; background: transparent; }
|
||||
.u-z-modal { z-index: 9998; }
|
||||
.u-z-modal-top { z-index: 9999; }
|
||||
|
|
@ -758,8 +1172,20 @@ body.theme-ds-body[data-admin-theme="light"] .content {
|
|||
.u-select-exec { min-width: 96px; max-width: 140px; padding: 6px 8px; }
|
||||
.u-inline-flex { display: inline-flex; align-items: center; }
|
||||
.u-switch-menu { margin: 2px 8px 8px; }
|
||||
.u-pop-menu { position: fixed; min-width: 220px; z-index: 300; }
|
||||
.u-pop-tip { position: fixed; max-width: 420px; white-space: pre-wrap; line-height: 1.4; }
|
||||
.u-pop-menu,
|
||||
.chat-sess-menu-pop.u-pop-menu {
|
||||
position: fixed;
|
||||
min-width: 220px;
|
||||
z-index: 300;
|
||||
}
|
||||
.u-pop-tip,
|
||||
.chat-sess-menu-pop.u-pop-tip {
|
||||
position: fixed;
|
||||
max-width: 420px;
|
||||
z-index: 300;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.u-empty-pad { padding: 10px 0 10px 8px; }
|
||||
.u-avatar-chip {
|
||||
width: 48px;
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ body.theme-ds-body {
|
|||
--chat-sess-hover-bg: rgba(255, 255, 255, 0.05);
|
||||
--chat-sess-active-bg: rgba(255, 255, 255, 0.062);
|
||||
--chat-sess-border: rgba(255, 255, 255, 0.09);
|
||||
/* 用户消息气泡(微信式绿底深字) */
|
||||
--chat-user-bubble-bg: #42b983;
|
||||
--chat-user-bubble-fg: #111111;
|
||||
/* 用户消息气泡:磨砂玻璃(半透明蓝) */
|
||||
--chat-user-bubble-bg: rgba(94, 179, 255, 0.18);
|
||||
--chat-user-bubble-fg: #e8e8e8;
|
||||
/* 弹层 / 菜单 / Toast */
|
||||
--ds-overlay-bg: rgba(0, 0, 0, 0.58);
|
||||
--ds-menu-hover-bg: rgba(255, 255, 255, 0.06);
|
||||
|
|
@ -330,10 +330,12 @@ body.theme-ds-body .card {
|
|||
}
|
||||
|
||||
.chat-sess-btn {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px 0 10px 8px;
|
||||
padding: 8px 6px 8px 8px;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border: 1px solid transparent;
|
||||
|
|
@ -341,6 +343,67 @@ body.theme-ds-body .card {
|
|||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-sess-btn__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-sess-btn__name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-sess-btn__id {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-sess-badge {
|
||||
flex: 0 0 2.6rem;
|
||||
width: 2.6rem;
|
||||
min-width: 2.6rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
padding: 4px 0;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Keep channel tint very subtle so the list reads as one system. */
|
||||
.chat-sess-badge--whatsapp {
|
||||
border-color: rgba(37, 211, 102, 0.28);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.chat-sess-badge--wechat,
|
||||
.chat-sess-badge--weixin {
|
||||
border-color: rgba(7, 193, 96, 0.28);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.chat-sess-badge--wecom {
|
||||
border-color: rgba(59, 130, 246, 0.28);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.chat-sess-badge--local {
|
||||
border-color: rgba(148, 163, 184, 0.28);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.chat-sess-btn:hover {
|
||||
|
|
@ -349,17 +412,19 @@ body.theme-ds-body .card {
|
|||
|
||||
.chat-sess-row {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.chat-sess-row:hover {
|
||||
background: var(--chat-sess-hover-bg, rgba(255, 255, 255, 0.06));
|
||||
border-color: var(--chat-sess-border, rgba(255, 255, 255, 0.12));
|
||||
background: var(--chat-sess-hover-bg, rgba(255, 255, 255, 0.05));
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.chat-sess-row--active {
|
||||
background: var(--chat-sess-active-bg, rgba(255, 255, 255, 0.062));
|
||||
border-color: var(--chat-sess-border, rgba(255, 255, 255, 0.08));
|
||||
background: var(--chat-sess-active-bg, rgba(148, 163, 184, 0.12));
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.chat-sess-row--active .chat-sess-btn {
|
||||
|
|
@ -369,11 +434,14 @@ body.theme-ds-body .card {
|
|||
.chat-sess-row .chat-sess-more {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
flex: 0 0 2rem;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.chat-sess-row:hover .chat-sess-more,
|
||||
.chat-sess-row:focus-within .chat-sess-more {
|
||||
opacity: 1;
|
||||
.chat-sess-row:focus-within .chat-sess-more,
|
||||
.chat-sess-row--active .chat-sess-more {
|
||||
opacity: 0.85;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
|
@ -398,10 +466,10 @@ body.theme-ds-body .card {
|
|||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 12px 14px;
|
||||
padding: 16px 18px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条:管理台等仍用隐藏式;独立 /chat 页见文件末尾 body.chat-standalone-page */
|
||||
|
|
@ -475,10 +543,10 @@ body.theme-ds-body .card {
|
|||
|
||||
.chat-msg {
|
||||
max-width: 92%;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
@ -589,9 +657,17 @@ body.theme-ds-body .card {
|
|||
|
||||
.chat-msg--user {
|
||||
align-self: flex-end;
|
||||
background: var(--chat-user-bubble-bg, #42b983);
|
||||
color: var(--chat-user-bubble-fg, #111);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
color: var(--chat-user-bubble-fg, #f1f5f9);
|
||||
background: var(--chat-user-bubble-bg, rgba(59, 130, 246, 0.22));
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
0 8px 24px rgba(2, 6, 23, 0.18);
|
||||
backdrop-filter: blur(14px) saturate(1.25);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.25);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.chat-msg--user .chat-msg__md,
|
||||
|
|
@ -600,22 +676,30 @@ body.theme-ds-body .card {
|
|||
}
|
||||
|
||||
.chat-msg--user .chat-msg__md a {
|
||||
color: #0b4d8c;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.chat-msg--user .chat-msg__md pre,
|
||||
.chat-msg--user .chat-msg__md code {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
color: #111;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
background: rgba(2, 6, 23, 0.28);
|
||||
color: #e2e8f0;
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.chat-msg--assistant {
|
||||
align-self: flex-start;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
background: var(--ds-splash-bg, rgba(148, 163, 184, 0.06));
|
||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
color: var(--ds-text, #e8e8e8);
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06),
|
||||
0 8px 24px rgba(2, 6, 23, 0.14);
|
||||
backdrop-filter: blur(14px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
||||
}
|
||||
|
||||
.chat-msg__actions {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ body.theme-ds-body[data-admin-theme="netx"] {
|
|||
--ds-text: #e2e8f0;
|
||||
--ds-text-muted: #94a3b8;
|
||||
--ds-accent: #3b82f6;
|
||||
--ds-accent-deep: #2563eb;
|
||||
--ds-accent-soft: rgba(59, 130, 246, 0.22);
|
||||
--ds-sidebar: #0c1220;
|
||||
--ds-topbar-bg: rgba(6, 10, 18, 0.94);
|
||||
|
|
@ -24,10 +25,10 @@ body.theme-ds-body[data-admin-theme="netx"] {
|
|||
--ds-chat-scroll-thumb: #475569;
|
||||
--ds-chat-scroll-thumb-hover: #64748b;
|
||||
--chat-sess-hover-bg: rgba(148, 163, 184, 0.08);
|
||||
--chat-sess-active-bg: rgba(59, 130, 246, 0.16);
|
||||
--chat-sess-active-bg: rgba(148, 163, 184, 0.12);
|
||||
--chat-sess-border: rgba(148, 163, 184, 0.18);
|
||||
--chat-user-bubble-bg: #2563eb;
|
||||
--chat-user-bubble-fg: #f8fafc;
|
||||
--chat-user-bubble-bg: rgba(59, 130, 246, 0.22);
|
||||
--chat-user-bubble-fg: #e2e8f0;
|
||||
--ds-overlay-bg: rgba(2, 6, 23, 0.72);
|
||||
--ds-menu-hover-bg: rgba(148, 163, 184, 0.1);
|
||||
--ds-menu-sep: rgba(148, 163, 184, 0.16);
|
||||
|
|
@ -226,6 +227,7 @@ body.theme-ds-body[data-admin-theme="light"] {
|
|||
--ds-splash-border: rgba(15, 23, 42, 0.12);
|
||||
--ds-splash-spinner-track: rgba(15, 23, 42, 0.12);
|
||||
--ds-splash-spinner-active: rgba(15, 23, 42, 0.55);
|
||||
--ds-composer-shell-bg: #f1f5f9;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .nav__groupTitle {
|
||||
|
|
@ -255,8 +257,32 @@ body.theme-ds-body[data-admin-theme="light"] .chat-avatar--userBuiltin {
|
|||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-msg--assistant {
|
||||
background: rgba(15, 23, 42, 0.045);
|
||||
border-color: var(--ds-border);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7),
|
||||
0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
backdrop-filter: blur(14px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-msg--user {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.55),
|
||||
0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
backdrop-filter: blur(14px) saturate(1.25);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.25);
|
||||
color: #0f172a;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-sess-badge--local {
|
||||
border-color: rgba(15, 23, 42, 0.2);
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
color: rgba(15, 23, 42, 0.72);
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-msg__action-btn {
|
||||
|
|
@ -293,6 +319,41 @@ body.theme-ds-body[data-admin-theme="light"] .chat-composer {
|
|||
border-top-color: rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip,
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
||||
background-color: #ffffff;
|
||||
border-color: rgba(15, 23, 42, 0.16);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
||||
background-color: #ffffff;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.55) 50%),
|
||||
linear-gradient(135deg, rgba(15, 23, 42, 0.55) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 14px) calc(50% - 2px),
|
||||
calc(100% - 9px) calc(50% - 2px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select option {
|
||||
background-color: #ffffff;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip:hover,
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:hover {
|
||||
background-color: #f8fafc;
|
||||
border-color: rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-shell {
|
||||
background: #f1f5f9;
|
||||
border-color: rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
body.theme-ds-body[data-admin-theme="light"] .chat-msg__reasoning {
|
||||
border-color: rgba(15, 23, 42, 0.1);
|
||||
background: rgba(15, 23, 42, 0.03);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue