mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
feat: 增强运行阶段托管通知反馈
This commit is contained in:
177
index.js
177
index.js
@@ -32,6 +32,7 @@ import {
|
|||||||
import { estimateTokens, formatInjection } from "./injector.js";
|
import { estimateTokens, formatInjection } from "./injector.js";
|
||||||
import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js";
|
import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js";
|
||||||
import { getNodeDisplayName } from "./node-labels.js";
|
import { getNodeDisplayName } from "./node-labels.js";
|
||||||
|
import { showManagedBmeNotice } from "./notice.js";
|
||||||
import { retrieve } from "./retriever.js";
|
import { retrieve } from "./retriever.js";
|
||||||
import {
|
import {
|
||||||
appendBatchJournal,
|
appendBatchJournal,
|
||||||
@@ -189,6 +190,12 @@ let sendIntentHookCleanup = [];
|
|||||||
let sendIntentHookRetryTimer = null;
|
let sendIntentHookRetryTimer = null;
|
||||||
let pendingHistoryRecoveryTimer = null;
|
let pendingHistoryRecoveryTimer = null;
|
||||||
let pendingHistoryRecoveryTrigger = "";
|
let pendingHistoryRecoveryTrigger = "";
|
||||||
|
const stageNoticeHandles = {
|
||||||
|
extraction: null,
|
||||||
|
vector: null,
|
||||||
|
recall: null,
|
||||||
|
history: null,
|
||||||
|
};
|
||||||
|
|
||||||
function createUiStatus(text = "待命", meta = "", level = "idle") {
|
function createUiStatus(text = "待命", meta = "", level = "idle") {
|
||||||
return {
|
return {
|
||||||
@@ -199,6 +206,99 @@ function createUiStatus(text = "待命", meta = "", level = "idle") {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStageNoticeLevel(level = "info") {
|
||||||
|
if (level === "running" || level === "idle") return "info";
|
||||||
|
if (level === "success" || level === "warning" || level === "error") {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStageNoticeTitle(stage) {
|
||||||
|
switch (stage) {
|
||||||
|
case "extraction":
|
||||||
|
return "ST-BME 提取";
|
||||||
|
case "vector":
|
||||||
|
return "ST-BME 向量";
|
||||||
|
case "recall":
|
||||||
|
return "ST-BME 召回";
|
||||||
|
case "history":
|
||||||
|
return "ST-BME 历史恢复";
|
||||||
|
default:
|
||||||
|
return "ST-BME";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStageNoticeDuration(level = "info") {
|
||||||
|
switch (level) {
|
||||||
|
case "error":
|
||||||
|
return 5600;
|
||||||
|
case "warning":
|
||||||
|
return 4600;
|
||||||
|
case "success":
|
||||||
|
return 2800;
|
||||||
|
default:
|
||||||
|
return 3200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNoticePanelAction() {
|
||||||
|
if (!_panelModule?.openPanel) return undefined;
|
||||||
|
return {
|
||||||
|
label: "打开面板",
|
||||||
|
kind: "neutral",
|
||||||
|
onClick: () => {
|
||||||
|
_panelModule?.openPanel?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissStageNotice(stage) {
|
||||||
|
stageNoticeHandles[stage]?.dismiss?.();
|
||||||
|
stageNoticeHandles[stage] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissAllStageNotices() {
|
||||||
|
for (const stage of Object.keys(stageNoticeHandles)) {
|
||||||
|
dismissStageNotice(stage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStageNotice(
|
||||||
|
stage,
|
||||||
|
text,
|
||||||
|
meta = "",
|
||||||
|
level = "info",
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const noticeLevel = normalizeStageNoticeLevel(level);
|
||||||
|
const busy = options.busy ?? level === "running";
|
||||||
|
const persist = options.persist ?? busy;
|
||||||
|
const title = options.title || getStageNoticeTitle(stage);
|
||||||
|
const message = [text, meta].filter(Boolean).join("\n");
|
||||||
|
const input = {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
level: noticeLevel,
|
||||||
|
busy,
|
||||||
|
persist,
|
||||||
|
duration_ms: options.duration_ms ?? getStageNoticeDuration(noticeLevel),
|
||||||
|
action:
|
||||||
|
options.action === undefined &&
|
||||||
|
(noticeLevel === "warning" || noticeLevel === "error")
|
||||||
|
? createNoticePanelAction()
|
||||||
|
: options.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentHandle = stageNoticeHandles[stage];
|
||||||
|
if (!currentHandle || currentHandle.isClosed?.()) {
|
||||||
|
stageNoticeHandles[stage] = showManagedBmeNotice(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHandle.update(input);
|
||||||
|
}
|
||||||
|
|
||||||
function createRecallInputRecord(overrides = {}) {
|
function createRecallInputRecord(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
text: "",
|
text: "",
|
||||||
@@ -427,6 +527,9 @@ function clearInjectionState() {
|
|||||||
lastRecalledItems = [];
|
lastRecalledItems = [];
|
||||||
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
||||||
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
||||||
|
if (!isRecalling) {
|
||||||
|
dismissStageNotice("recall");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
@@ -473,6 +576,9 @@ function setLastExtractionStatus(
|
|||||||
} else {
|
} else {
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
|
updateStageNotice("extraction", text, meta, level, {
|
||||||
|
title: toastTitle,
|
||||||
|
});
|
||||||
if (toastKind) {
|
if (toastKind) {
|
||||||
notifyStatusToast(`extract:${toastKind}`, toastKind, meta || text, toastTitle);
|
notifyStatusToast(`extract:${toastKind}`, toastKind, meta || text, toastTitle);
|
||||||
}
|
}
|
||||||
@@ -490,6 +596,9 @@ function setLastVectorStatus(
|
|||||||
} else {
|
} else {
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
|
updateStageNotice("vector", text, meta, level, {
|
||||||
|
title: toastTitle,
|
||||||
|
});
|
||||||
if (toastKind) {
|
if (toastKind) {
|
||||||
notifyStatusToast(`vector:${toastKind}`, toastKind, meta || text, toastTitle);
|
notifyStatusToast(`vector:${toastKind}`, toastKind, meta || text, toastTitle);
|
||||||
}
|
}
|
||||||
@@ -507,6 +616,9 @@ function setLastRecallStatus(
|
|||||||
} else {
|
} else {
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
|
updateStageNotice("recall", text, meta, level, {
|
||||||
|
title: toastTitle,
|
||||||
|
});
|
||||||
if (toastKind) {
|
if (toastKind) {
|
||||||
notifyStatusToast(`recall:${toastKind}`, toastKind, meta || text, toastTitle);
|
notifyStatusToast(`recall:${toastKind}`, toastKind, meta || text, toastTitle);
|
||||||
}
|
}
|
||||||
@@ -881,6 +993,7 @@ function updateModuleSettings(patch = {}) {
|
|||||||
Object.prototype.hasOwnProperty.call(patch, "enabled") &&
|
Object.prototype.hasOwnProperty.call(patch, "enabled") &&
|
||||||
patch.enabled === false
|
patch.enabled === false
|
||||||
) {
|
) {
|
||||||
|
dismissAllStageNotices();
|
||||||
try {
|
try {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
context.setExtensionPrompt(
|
context.setExtensionPrompt(
|
||||||
@@ -1326,6 +1439,16 @@ function getLastProcessedAssistantFloor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyHistoryDirty(dirtyFrom, reason) {
|
function notifyHistoryDirty(dirtyFrom, reason) {
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"检测到楼层历史变化",
|
||||||
|
`将从楼层 ${dirtyFrom} 之后自动恢复${reason ? `\n${reason}` : ""}`,
|
||||||
|
"warning",
|
||||||
|
{
|
||||||
|
persist: true,
|
||||||
|
busy: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastHistoryWarningAt < 3000) return;
|
if (now - lastHistoryWarningAt < 3000) return;
|
||||||
lastHistoryWarningAt = now;
|
lastHistoryWarningAt = now;
|
||||||
@@ -1355,6 +1478,16 @@ function scheduleImmediateHistoryRecovery(
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[ST-BME] 事件触发的历史恢复失败:", error);
|
console.error("[ST-BME] 事件触发的历史恢复失败:", error);
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"历史恢复失败",
|
||||||
|
error?.message || String(error),
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
busy: false,
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
toastr.error(`历史恢复失败: ${error?.message || error}`);
|
toastr.error(`历史恢复失败: ${error?.message || error}`);
|
||||||
});
|
});
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
@@ -1561,6 +1694,19 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
let replayedBatches = 0;
|
let replayedBatches = 0;
|
||||||
let usedFullRebuild = false;
|
let usedFullRebuild = false;
|
||||||
|
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"历史恢复中",
|
||||||
|
Number.isFinite(initialDirtyFrom)
|
||||||
|
? `受影响起点楼层 ${initialDirtyFrom} · 正在回滚并重放`
|
||||||
|
: "正在回滚并重放受影响后缀",
|
||||||
|
"running",
|
||||||
|
{
|
||||||
|
persist: true,
|
||||||
|
busy: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const recoveryPoint = findJournalRecoveryPoint(currentGraph, initialDirtyFrom);
|
const recoveryPoint = findJournalRecoveryPoint(currentGraph, initialDirtyFrom);
|
||||||
if (recoveryPoint) {
|
if (recoveryPoint) {
|
||||||
@@ -1586,6 +1732,16 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成",
|
||||||
|
`起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`,
|
||||||
|
usedFullRebuild ? "warning" : "success",
|
||||||
|
{
|
||||||
|
busy: false,
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toastr.success(
|
toastr.success(
|
||||||
usedFullRebuild
|
usedFullRebuild
|
||||||
@@ -1610,6 +1766,16 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"历史恢复已退化为全量重建",
|
||||||
|
`起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`,
|
||||||
|
"warning",
|
||||||
|
{
|
||||||
|
busy: false,
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
toastr.warning("历史恢复已退化为全量重建");
|
toastr.warning("历史恢复已退化为全量重建");
|
||||||
return true;
|
return true;
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
@@ -1619,6 +1785,16 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
});
|
});
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"历史恢复失败",
|
||||||
|
fallbackError?.message || String(fallbackError),
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
busy: false,
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
toastr.error(`历史恢复失败: ${fallbackError?.message || fallbackError}`);
|
toastr.error(`历史恢复失败: ${fallbackError?.message || fallbackError}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1863,6 +2039,7 @@ function onChatChanged() {
|
|||||||
clearTimeout(pendingHistoryRecoveryTimer);
|
clearTimeout(pendingHistoryRecoveryTimer);
|
||||||
pendingHistoryRecoveryTimer = null;
|
pendingHistoryRecoveryTimer = null;
|
||||||
pendingHistoryRecoveryTrigger = "";
|
pendingHistoryRecoveryTrigger = "";
|
||||||
|
dismissAllStageNotices();
|
||||||
loadGraphFromChat();
|
loadGraphFromChat();
|
||||||
clearInjectionState();
|
clearInjectionState();
|
||||||
clearRecallInputTracking();
|
clearRecallInputTracking();
|
||||||
|
|||||||
424
notice.js
Normal file
424
notice.js
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
const STYLE_ID = "st-bme-notice-style";
|
||||||
|
const HOST_ID = "st-bme-notice-host";
|
||||||
|
|
||||||
|
function resolveNoticeDocument() {
|
||||||
|
const runtime = globalThis;
|
||||||
|
const chatDocument = runtime?.SillyTavern?.Chat?.document;
|
||||||
|
if (chatDocument && typeof chatDocument.createElement === "function") {
|
||||||
|
return chatDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (window.parent && window.parent !== window ? window.parent : window).document;
|
||||||
|
} catch {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStyle(doc) {
|
||||||
|
if (doc.getElementById(STYLE_ID)) return;
|
||||||
|
|
||||||
|
const style = doc.createElement("style");
|
||||||
|
style.id = STYLE_ID;
|
||||||
|
style.textContent = `
|
||||||
|
#${HOST_ID} {
|
||||||
|
position: fixed;
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
z-index: 12020;
|
||||||
|
width: min(400px, calc(100vw - 28px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice {
|
||||||
|
--st-bme-accent: #73b8ff;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 12px 12px 12px 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% -10%, rgba(115, 184, 255, 0.2), transparent 52%),
|
||||||
|
linear-gradient(145deg, rgba(27, 37, 54, 0.95), rgba(12, 18, 29, 0.93));
|
||||||
|
box-shadow:
|
||||||
|
0 14px 34px rgba(4, 10, 17, 0.46),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||||
|
color: #edf3fb;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(-8px) scale(0.985);
|
||||||
|
opacity: 0;
|
||||||
|
animation: stBmeNoticeIn 190ms ease forwards;
|
||||||
|
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", sans-serif;
|
||||||
|
backdrop-filter: blur(10px) saturate(125%);
|
||||||
|
-webkit-backdrop-filter: blur(10px) saturate(125%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-left: 3px solid var(--st-bme-accent);
|
||||||
|
border-radius: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice--out {
|
||||||
|
animation: stBmeNoticeOut 160ms ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #f4f8ff;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice[data-busy="true"] .st-bme-notice__icon {
|
||||||
|
animation: stBmeNoticeBusy 900ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.18;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: #f0f6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__message {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.38;
|
||||||
|
color: rgba(240, 246, 255, 0.86);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__action {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #eef4ff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 140ms ease, border-color 140ms ease, transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__action:hover,
|
||||||
|
.st-bme-notice__action:focus-visible {
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
border-color: rgba(255, 255, 255, 0.24);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__action[data-kind="danger"] {
|
||||||
|
background: rgba(245, 123, 143, 0.16);
|
||||||
|
border-color: rgba(245, 123, 143, 0.42);
|
||||||
|
color: #ffd9df;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__close {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #d7e0ec;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__close:hover,
|
||||||
|
.st-bme-notice__close:focus-visible {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice__progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--st-bme-accent), rgba(255, 255, 255, 0.24));
|
||||||
|
transform-origin: left center;
|
||||||
|
animation: stBmeNoticeProgress linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice[data-level="success"] {
|
||||||
|
--st-bme-accent: #65d39c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice[data-level="error"] {
|
||||||
|
--st-bme-accent: #f57b8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-bme-notice[data-level="warning"] {
|
||||||
|
--st-bme-accent: #eab96f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stBmeNoticeIn {
|
||||||
|
to {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stBmeNoticeOut {
|
||||||
|
to {
|
||||||
|
transform: translateY(-6px) scale(0.98);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stBmeNoticeProgress {
|
||||||
|
from {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stBmeNoticeBusy {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#${HOST_ID} {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: calc(100vw - 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.st-bme-notice,
|
||||||
|
.st-bme-notice--out,
|
||||||
|
.st-bme-notice__progress {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
(doc.head || doc.documentElement).appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureHost(doc) {
|
||||||
|
let host = doc.getElementById(HOST_ID);
|
||||||
|
if (host) return host;
|
||||||
|
|
||||||
|
host = doc.createElement("div");
|
||||||
|
host.id = HOST_ID;
|
||||||
|
host.setAttribute("aria-live", "polite");
|
||||||
|
host.setAttribute("aria-atomic", "false");
|
||||||
|
(doc.body || doc.documentElement).appendChild(host);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(level) {
|
||||||
|
switch (level) {
|
||||||
|
case "success":
|
||||||
|
return "✓";
|
||||||
|
case "error":
|
||||||
|
return "!";
|
||||||
|
case "warning":
|
||||||
|
return "△";
|
||||||
|
default:
|
||||||
|
return "i";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNoticeState(item, input, progress) {
|
||||||
|
const level = input.level || "info";
|
||||||
|
item.dataset.level = level;
|
||||||
|
item.dataset.busy = input.busy ? "true" : "false";
|
||||||
|
|
||||||
|
const icon = item.querySelector(".st-bme-notice__icon");
|
||||||
|
if (icon) {
|
||||||
|
icon.textContent = input.busy ? "◌" : getIcon(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = item.querySelector(".st-bme-notice__title");
|
||||||
|
if (title) {
|
||||||
|
title.textContent = input.title || "ST-BME";
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = item.querySelector(".st-bme-notice__message");
|
||||||
|
if (message) {
|
||||||
|
message.textContent = input.message || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionWrap = item.querySelector(".st-bme-notice__actions");
|
||||||
|
const actionButton = item.querySelector(".st-bme-notice__action");
|
||||||
|
if (actionWrap && actionButton) {
|
||||||
|
if (input.action?.label) {
|
||||||
|
actionWrap.style.display = "";
|
||||||
|
actionButton.style.display = "";
|
||||||
|
actionButton.textContent = input.action.label;
|
||||||
|
actionButton.dataset.kind = input.action.kind || "neutral";
|
||||||
|
} else {
|
||||||
|
actionWrap.style.display = "none";
|
||||||
|
actionButton.style.display = "none";
|
||||||
|
actionButton.textContent = "";
|
||||||
|
actionButton.dataset.kind = "neutral";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.persist) {
|
||||||
|
progress.style.display = "none";
|
||||||
|
progress.style.animationDuration = "";
|
||||||
|
} else {
|
||||||
|
const duration = Math.max(1400, input.duration_ms || 3200);
|
||||||
|
progress.style.display = "";
|
||||||
|
progress.style.animationDuration = `${duration}ms`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showManagedBmeNotice(input) {
|
||||||
|
const doc = resolveNoticeDocument();
|
||||||
|
ensureStyle(doc);
|
||||||
|
const host = ensureHost(doc);
|
||||||
|
|
||||||
|
const item = doc.createElement("article");
|
||||||
|
item.className = "st-bme-notice";
|
||||||
|
|
||||||
|
const icon = doc.createElement("span");
|
||||||
|
icon.className = "st-bme-notice__icon";
|
||||||
|
|
||||||
|
const content = doc.createElement("div");
|
||||||
|
content.className = "st-bme-notice__content";
|
||||||
|
|
||||||
|
const title = doc.createElement("h4");
|
||||||
|
title.className = "st-bme-notice__title";
|
||||||
|
|
||||||
|
const message = doc.createElement("p");
|
||||||
|
message.className = "st-bme-notice__message";
|
||||||
|
|
||||||
|
const actions = doc.createElement("div");
|
||||||
|
actions.className = "st-bme-notice__actions";
|
||||||
|
|
||||||
|
const actionButton = doc.createElement("button");
|
||||||
|
actionButton.className = "st-bme-notice__action";
|
||||||
|
actionButton.type = "button";
|
||||||
|
actionButton.style.display = "none";
|
||||||
|
|
||||||
|
const closeButton = doc.createElement("button");
|
||||||
|
closeButton.className = "st-bme-notice__close";
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.setAttribute("aria-label", "关闭提示");
|
||||||
|
closeButton.textContent = "×";
|
||||||
|
|
||||||
|
const progress = doc.createElement("div");
|
||||||
|
progress.className = "st-bme-notice__progress";
|
||||||
|
|
||||||
|
content.appendChild(title);
|
||||||
|
content.appendChild(message);
|
||||||
|
actions.appendChild(actionButton);
|
||||||
|
content.appendChild(actions);
|
||||||
|
item.appendChild(icon);
|
||||||
|
item.appendChild(content);
|
||||||
|
item.appendChild(closeButton);
|
||||||
|
item.appendChild(progress);
|
||||||
|
|
||||||
|
let currentInput = input || {};
|
||||||
|
let closed = false;
|
||||||
|
let closeTimer = null;
|
||||||
|
|
||||||
|
const clearCloseTimer = () => {
|
||||||
|
if (!closeTimer) return;
|
||||||
|
clearTimeout(closeTimer);
|
||||||
|
closeTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (closed) return;
|
||||||
|
clearCloseTimer();
|
||||||
|
closed = true;
|
||||||
|
item.classList.add("st-bme-notice--out");
|
||||||
|
setTimeout(() => {
|
||||||
|
item.remove();
|
||||||
|
if (!host.childElementCount) {
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
}, 170);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleAutoClose = (nextInput) => {
|
||||||
|
clearCloseTimer();
|
||||||
|
if (nextInput.persist) return;
|
||||||
|
const duration = Math.max(1400, nextInput.duration_ms || 3200);
|
||||||
|
closeTimer = setTimeout(close, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = (nextInput) => {
|
||||||
|
if (closed) return;
|
||||||
|
currentInput = nextInput || {};
|
||||||
|
applyNoticeState(item, currentInput, progress);
|
||||||
|
scheduleAutoClose(currentInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
applyNoticeState(item, currentInput, progress);
|
||||||
|
scheduleAutoClose(currentInput);
|
||||||
|
|
||||||
|
actionButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
currentInput.action?.onClick?.();
|
||||||
|
});
|
||||||
|
closeButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
host.appendChild(item);
|
||||||
|
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
dismiss: close,
|
||||||
|
isClosed: () => closed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showBmeNotice(input) {
|
||||||
|
return showManagedBmeNotice(input);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user