(() => {
// frontend/js/components/slidingTabs.js
var SlidingTabs = class {
/**
* @param {HTMLElement} containerElement - The main container element with the `data-sliding-tabs-container` attribute.
*/
constructor(containerElement) {
this.container = containerElement;
this.indicator = this.container.querySelector("[data-tab-indicator]");
this.tabs = this.container.querySelectorAll("[data-tab-item]");
this.activeTab = this.container.querySelector(".tab-active");
if (!this.indicator || this.tabs.length === 0) {
console.error("SlidingTabs component is missing required elements (indicator or items).", this.container);
return;
}
this.init();
}
init() {
if (this.activeTab) {
setTimeout(() => this.updateIndicator(this.activeTab), 50);
}
this.bindEvents();
}
updateIndicator(targetTab) {
if (!targetTab) return;
const containerRect = this.container.getBoundingClientRect();
const targetRect = targetTab.getBoundingClientRect();
const left = targetRect.left - containerRect.left;
const width = targetRect.width;
this.indicator.style.left = `${left}px`;
this.indicator.style.width = `${width}px`;
}
bindEvents() {
this.tabs.forEach((tab) => {
tab.addEventListener("click", (e) => {
if (this.activeTab) {
this.activeTab.classList.remove("tab-active");
}
tab.classList.add("tab-active");
this.activeTab = tab;
this.updateIndicator(this.activeTab);
});
tab.addEventListener("mouseenter", () => {
this.updateIndicator(tab);
});
});
this.container.addEventListener("mouseleave", () => {
this.updateIndicator(this.activeTab);
});
}
};
document.addEventListener("DOMContentLoaded", () => {
const allTabContainers = document.querySelectorAll("[data-sliding-tabs-container]");
allTabContainers.forEach((container) => {
new SlidingTabs(container);
});
});
// frontend/js/components/customSelect.js
var CustomSelect = class _CustomSelect {
constructor(container) {
this.container = container;
this.trigger = this.container.querySelector(".custom-select-trigger");
this.panel = this.container.querySelector(".custom-select-panel");
if (!this.trigger || !this.panel) {
console.warn("CustomSelect cannot initialize: missing .custom-select-trigger or .custom-select-panel.", this.container);
return;
}
this.nativeSelect = this.container.querySelector("select");
this.triggerText = this.trigger.querySelector("span");
this.template = this.panel.querySelector(".custom-select-option-template");
if (typeof _CustomSelect.openInstance === "undefined") {
_CustomSelect.openInstance = null;
_CustomSelect.initGlobalListener();
}
if (this.nativeSelect) {
this.generateOptions();
this.updateTriggerText();
}
this.bindEvents();
}
static initGlobalListener() {
document.addEventListener("click", (event) => {
if (_CustomSelect.openInstance && !_CustomSelect.openInstance.container.contains(event.target)) {
_CustomSelect.openInstance.close();
}
});
}
generateOptions() {
this.panel.querySelectorAll(":scope > *:not(.custom-select-option-template)").forEach((child) => child.remove());
Array.from(this.nativeSelect.options).forEach((option) => {
let item;
if (this.template) {
item = this.template.cloneNode(true);
item.classList.remove("custom-select-option-template");
item.removeAttribute("hidden");
} else {
item = document.createElement("a");
item.href = "#";
item.className = "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-600";
}
item.classList.add("custom-select-option");
item.textContent = option.textContent;
item.dataset.value = option.value;
if (option.selected) {
item.classList.add("is-selected");
}
this.panel.appendChild(item);
});
}
bindEvents() {
this.trigger.addEventListener("click", (event) => {
if (this.trigger.classList.contains("is-disabled")) {
return;
}
event.stopPropagation();
if (_CustomSelect.openInstance && _CustomSelect.openInstance !== this) {
_CustomSelect.openInstance.close();
}
this.toggle();
});
if (this.nativeSelect) {
this.panel.addEventListener("click", (event) => {
event.preventDefault();
const option = event.target.closest(".custom-select-option");
if (option) {
this.selectOption(option);
}
});
}
}
selectOption(optionEl) {
const selectedValue = optionEl.dataset.value;
if (this.nativeSelect.value !== selectedValue) {
this.nativeSelect.value = selectedValue;
this.nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
this.updateTriggerText();
this.panel.querySelectorAll(".custom-select-option").forEach((el) => el.classList.remove("is-selected"));
optionEl.classList.add("is-selected");
this.close();
}
updateTriggerText() {
if (!this.nativeSelect || !this.triggerText) return;
const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
if (selectedOption) {
this.triggerText.textContent = selectedOption.textContent;
}
}
toggle() {
this.panel.classList.toggle("hidden");
if (this.panel.classList.contains("hidden")) {
if (_CustomSelect.openInstance === this) {
_CustomSelect.openInstance = null;
}
} else {
_CustomSelect.openInstance = this;
}
}
open() {
this.panel.classList.remove("hidden");
_CustomSelect.openInstance = this;
}
close() {
this.panel.classList.add("hidden");
if (_CustomSelect.openInstance === this) {
_CustomSelect.openInstance = null;
}
}
};
// frontend/js/components/ui.js
var ModalManager = class {
/**
* Shows a generic modal by its ID.
* @param {string} modalId The ID of the modal element to show.
*/
show(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove("hidden");
} else {
console.error(`Modal with ID "${modalId}" not found.`);
}
}
/**
* Hides a generic modal by its ID.
* @param {string} modalId The ID of the modal element to hide.
*/
hide(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add("hidden");
} else {
console.error(`Modal with ID "${modalId}" not found.`);
}
}
/**
* Shows a confirmation dialog. This is a versatile method for 'Are you sure?' style prompts.
* It dynamically sets the title, message, and confirm action for a generic confirmation modal.
* @param {object} options - The options for the confirmation modal.
* @param {string} options.modalId - The ID of the confirmation modal element (e.g., 'resetModal', 'deleteConfirmModal').
* @param {string} options.title - The title to display in the modal header.
* @param {string} options.message - The message to display in the modal body. Can contain HTML.
* @param {function} options.onConfirm - The callback function to execute when the confirm button is clicked.
* @param {boolean} [options.disableConfirm=false] - Whether the confirm button should be initially disabled.
*/
showConfirm({ modalId, title, message, onConfirm, disableConfirm = false }) {
const modalElement = document.getElementById(modalId);
if (!modalElement) {
console.error(`Confirmation modal with ID "${modalId}" not found.`);
return;
}
const titleElement = modalElement.querySelector('[id$="ModalTitle"]');
const messageElement = modalElement.querySelector('[id$="ModalMessage"]');
const confirmButton = modalElement.querySelector('[id^="confirm"]');
if (!titleElement || !messageElement || !confirmButton) {
console.error(`Modal "${modalId}" is missing required child elements (title, message, or confirm button).`);
return;
}
titleElement.textContent = title;
messageElement.innerHTML = message;
confirmButton.disabled = disableConfirm;
const newConfirmButton = confirmButton.cloneNode(true);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
newConfirmButton.onclick = () => onConfirm();
this.show(modalId);
}
/**
* Shows a result modal to indicate the outcome of an operation (success or failure).
* @param {boolean} success - If true, displays a success icon and title; otherwise, shows failure indicators.
* @param {string|Node} message - The message to display. Can be a simple string or a complex DOM Node for rich content.
* @param {boolean} [autoReload=false] - If true, the page will automatically reload when the modal is closed.
*/
showResult(success, message, autoReload = false) {
const modalElement = document.getElementById("resultModal");
if (!modalElement) {
console.error("Result modal with ID 'resultModal' not found.");
return;
}
const titleElement = document.getElementById("resultModalTitle");
const messageElement = document.getElementById("resultModalMessage");
const iconElement = document.getElementById("resultIcon");
const confirmButton = document.getElementById("resultModalConfirmBtn");
if (!titleElement || !messageElement || !iconElement || !confirmButton) {
console.error("Result modal is missing required child elements.");
return;
}
titleElement.textContent = success ? "\u64CD\u4F5C\u6210\u529F" : "\u64CD\u4F5C\u5931\u8D25";
if (success) {
iconElement.innerHTML = '';
iconElement.className = "text-6xl mb-3 text-success-500";
} else {
iconElement.innerHTML = '';
iconElement.className = "text-6xl mb-3 text-danger-500";
}
messageElement.innerHTML = "";
if (typeof message === "string") {
const messageDiv = document.createElement("div");
messageDiv.innerText = message;
messageElement.appendChild(messageDiv);
} else if (message instanceof Node) {
messageElement.appendChild(message);
} else {
const messageDiv = document.createElement("div");
messageDiv.innerText = String(message);
messageElement.appendChild(messageDiv);
}
confirmButton.onclick = () => this.closeResult(autoReload);
this.show("resultModal");
}
/**
* Closes the result modal.
* @param {boolean} [reload=false] - If true, reloads the page after closing the modal.
*/
closeResult(reload = false) {
this.hide("resultModal");
if (reload) {
location.reload();
}
}
/**
* Shows and initializes the progress modal for long-running operations.
* @param {string} title - The title to display for the progress modal.
*/
showProgress(title) {
const modal = document.getElementById("progressModal");
if (!modal) {
console.error("Progress modal with ID 'progressModal' not found.");
return;
}
const titleElement = document.getElementById("progressModalTitle");
const statusText = document.getElementById("progressStatusText");
const progressBar = document.getElementById("progressBar");
const progressPercentage = document.getElementById("progressPercentage");
const progressLog = document.getElementById("progressLog");
const closeButton = document.getElementById("progressModalCloseBtn");
const closeIcon = document.getElementById("closeProgressModalBtn");
if (!titleElement || !statusText || !progressBar || !progressPercentage || !progressLog || !closeButton || !closeIcon) {
console.error("Progress modal is missing required child elements.");
return;
}
titleElement.textContent = title;
statusText.textContent = "\u51C6\u5907\u5F00\u59CB...";
progressBar.style.width = "0%";
progressPercentage.textContent = "0%";
progressLog.innerHTML = "";
closeButton.disabled = true;
closeIcon.disabled = true;
this.show("progressModal");
}
/**
* Updates the progress bar and status text within the progress modal.
* @param {number} processed - The number of items that have been processed.
* @param {number} total - The total number of items to process.
* @param {string} status - The current status message to display.
*/
updateProgress(processed, total, status) {
const modal = document.getElementById("progressModal");
if (!modal || modal.classList.contains("hidden")) return;
const progressBar = document.getElementById("progressBar");
const progressPercentage = document.getElementById("progressPercentage");
const statusText = document.getElementById("progressStatusText");
const closeButton = document.getElementById("progressModalCloseBtn");
const closeIcon = document.getElementById("closeProgressModalBtn");
const percentage = total > 0 ? Math.round(processed / total * 100) : 0;
progressBar.style.width = `${percentage}%`;
progressPercentage.textContent = `${percentage}%`;
statusText.textContent = status;
if (processed === total) {
closeButton.disabled = false;
closeIcon.disabled = false;
}
}
/**
* Adds a log entry to the progress modal's log area.
* @param {string} message - The log message to append.
* @param {boolean} [isError=false] - If true, styles the log entry as an error.
*/
addProgressLog(message, isError = false) {
const progressLog = document.getElementById("progressLog");
if (!progressLog) return;
const logEntry = document.createElement("div");
logEntry.textContent = message;
logEntry.className = isError ? "text-danger-600" : "text-gray-700";
progressLog.appendChild(logEntry);
progressLog.scrollTop = progressLog.scrollHeight;
}
/**
* Closes the progress modal.
* @param {boolean} [reload=false] - If true, reloads the page after closing.
*/
closeProgress(reload = false) {
this.hide("progressModal");
if (reload) {
location.reload();
}
}
};
var UIPatterns = class {
/**
* Animates numerical values in elements from 0 to their target number.
* The target number is read from the element's text content.
* @param {string} selector - The CSS selector for the elements to animate (e.g., '.stat-value').
* @param {number} [duration=1500] - The duration of the animation in milliseconds.
*/
animateCounters(selector = ".stat-value", duration = 1500) {
const statValues = document.querySelectorAll(selector);
statValues.forEach((valueElement) => {
const finalValue = parseInt(valueElement.textContent, 10);
if (isNaN(finalValue)) return;
if (!valueElement.dataset.originalValue) {
valueElement.dataset.originalValue = valueElement.textContent;
}
let startValue = 0;
const startTime = performance.now();
const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
const progress = elapsedTime / duration;
const easeOutValue = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.floor(easeOutValue * finalValue);
valueElement.textContent = currentValue;
requestAnimationFrame(updateCounter);
} else {
valueElement.textContent = valueElement.dataset.originalValue;
}
};
requestAnimationFrame(updateCounter);
});
}
/**
* Toggles the visibility of a content section with a smooth height animation.
* It expects a specific HTML structure where the header and content are within a common parent (e.g., a card).
* The content element should have a `collapsed` class when hidden.
* @param {HTMLElement} header - The header element that was clicked to trigger the toggle.
*/
toggleSection(header) {
const card = header.closest(".stats-card");
if (!card) return;
const content = card.querySelector(".key-content");
const toggleIcon = header.querySelector(".toggle-icon");
if (!content || !toggleIcon) {
console.error("Toggle section failed: Content or icon element not found.", { header });
return;
}
const isCollapsed = content.classList.contains("collapsed");
toggleIcon.classList.toggle("collapsed", !isCollapsed);
if (isCollapsed) {
content.classList.remove("collapsed");
content.style.maxHeight = null;
content.style.opacity = null;
content.style.paddingTop = null;
content.style.paddingBottom = null;
content.style.overflow = "hidden";
requestAnimationFrame(() => {
const targetHeight = content.scrollHeight;
content.style.maxHeight = `${targetHeight}px`;
content.style.opacity = "1";
content.style.paddingTop = "1rem";
content.style.paddingBottom = "1rem";
content.addEventListener("transitionend", function onExpansionEnd() {
content.removeEventListener("transitionend", onExpansionEnd);
if (!content.classList.contains("collapsed")) {
content.style.maxHeight = "";
content.style.overflow = "visible";
}
}, { once: true });
});
} else {
const currentHeight = content.scrollHeight;
content.style.maxHeight = `${currentHeight}px`;
content.style.overflow = "hidden";
requestAnimationFrame(() => {
content.style.maxHeight = "0px";
content.style.opacity = "0";
content.style.paddingTop = "0";
content.style.paddingBottom = "0";
content.classList.add("collapsed");
});
}
}
};
var modalManager = new ModalManager();
var uiPatterns = new UIPatterns();
// frontend/js/components/taskCenter.js
var TaskCenterManager = class {
constructor() {
this.tasks = [];
this.activePolls = /* @__PURE__ */ new Map();
this.heartbeatInterval = null;
this.MINIMUM_TASK_DISPLAY_TIME_MS = 800;
this.hasUnreadCompletedTasks = false;
this.isAnimating = false;
this.countdownTimer = null;
this.trigger = document.getElementById("task-hub-trigger");
this.panel = document.getElementById("task-hub-panel");
this.countdownBar = document.getElementById("task-hub-countdown-bar");
this.countdownRing = document.getElementById("task-hub-countdown-ring");
this.indicator = document.getElementById("task-hub-indicator");
this.clearBtn = document.getElementById("task-hub-clear-btn");
this.taskListContainer = document.getElementById("task-list-container");
this.emptyState = document.getElementById("task-list-empty");
}
// [THE FINAL, DEFINITIVE VERSION]
init() {
if (!this.trigger || !this.panel) {
console.warn("Task Center UI core elements not found. Initialization skipped.");
return;
}
this.trigger.addEventListener("click", (event) => {
event.stopPropagation();
if (this.isAnimating) return;
if (this.panel.classList.contains("hidden")) {
this._handleUserInteraction();
this.openPanel();
} else {
this.closePanel();
}
});
document.addEventListener("click", (event) => {
if (!this.panel.classList.contains("hidden") && !this.isAnimating && !this.panel.contains(event.target) && !this.trigger.contains(event.target)) {
this.closePanel();
}
});
this.trigger.addEventListener("mouseenter", this._stopCountdown.bind(this));
this.panel.addEventListener("mouseenter", this._handleUserInteraction.bind(this));
const handleMouseLeave = () => {
if (!this.panel.classList.contains("hidden")) {
this._startCountdown();
}
};
this.panel.addEventListener("mouseleave", handleMouseLeave);
this.trigger.addEventListener("mouseleave", handleMouseLeave);
this.clearBtn?.addEventListener("click", this.clearCompletedTasks.bind(this));
this.taskListContainer.addEventListener("click", (event) => {
const toggleHeader = event.target.closest("[data-task-toggle]");
if (!toggleHeader) return;
this._handleUserInteraction();
const taskItem = toggleHeader.closest(".task-list-item");
const content = taskItem.querySelector("[data-task-content]");
if (!content) return;
const isCollapsed = content.classList.contains("collapsed");
toggleHeader.classList.toggle("expanded", isCollapsed);
if (isCollapsed) {
content.classList.remove("collapsed");
content.style.maxHeight = `${content.scrollHeight}px`;
content.style.opacity = "1";
content.addEventListener("transitionend", () => {
if (!content.classList.contains("collapsed")) content.style.maxHeight = "none";
}, { once: true });
} else {
content.style.maxHeight = `${content.scrollHeight}px`;
requestAnimationFrame(() => {
content.style.maxHeight = "0px";
content.style.opacity = "0";
content.classList.add("collapsed");
});
}
});
this._render();
this._startHeartbeat();
console.log("Task Center UI Initialized [Multi-Task Heartbeat Polling Architecture - IGNITED].");
}
async startTask(taskDefinition) {
try {
const initialTaskData = await taskDefinition.start();
if (!initialTaskData || !initialTaskData.id) throw new Error("Task definition did not return a valid initial task object.");
const newTask = {
id: initialTaskData.id,
definition: taskDefinition,
data: initialTaskData,
timestamp: /* @__PURE__ */ new Date(),
startTime: Date.now()
};
if (!initialTaskData.is_running) {
console.log(`[TaskCenter] Task ${newTask.id} completed synchronously. Skipping poll.`);
taskDefinition.renderToastNarrative(newTask.data, {}, toastManager);
this.tasks.unshift(newTask);
this._render();
this._handleTaskCompletion(newTask);
return;
}
this.tasks.unshift(newTask);
this.activePolls.set(newTask.id, newTask);
this._render();
this.openPanel();
taskDefinition.renderToastNarrative(newTask.data, {}, toastManager);
this._updateIndicatorState();
} catch (error) {
console.error("Failed to start task:", error);
toastManager.show(`\u4EFB\u52A1\u542F\u52A8\u5931\u8D25: ${error.message}`, "error");
}
}
_startHeartbeat() {
if (this.heartbeatInterval) return;
this.heartbeatInterval = setInterval(this._tick.bind(this), 1500);
}
_stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
async _tick() {
if (this.activePolls.size === 0) {
return;
}
for (const taskId of [...this.activePolls.keys()]) {
const task = this.activePolls.get(taskId);
if (!task) continue;
try {
const response = await task.definition.poll(taskId);
if (!response.success || !response.data) throw new Error(response.message || "Polling failed");
const oldData = { ...task.data };
task.data = response.data;
this._updateTaskItemInHistory(task.id, task.data);
task.definition.renderToastNarrative(task.data, oldData, toastManager);
if (!task.data.is_running) {
this._handleTaskCompletion(task);
}
} catch (error) {
console.error(`Polling for task ${taskId} failed:`, error);
task.data.error = error.message;
this._updateTaskItemInHistory(task.id, task.data);
this._handleTaskCompletion(task);
}
}
}
_handleTaskCompletion(task) {
this.activePolls.delete(task.id);
this._updateIndicatorState();
const toastId = `task-${task.id}`;
const finalize = async () => {
await toastManager.dismiss(toastId, !task.data.error);
this._updateTaskItemInDom(task);
this.hasUnreadCompletedTasks = true;
this._updateIndicatorState();
if (task.data.error) {
if (task.definition.onError) task.definition.onError(task.data);
} else {
if (task.definition.onSuccess) task.definition.onSuccess(task.data);
}
};
const elapsedTime = Date.now() - task.startTime;
const remainingTime = this.MINIMUM_TASK_DISPLAY_TIME_MS - elapsedTime;
if (remainingTime > 0) {
setTimeout(finalize, remainingTime);
} else {
finalize();
}
}
// [REFACTORED for robustness]
_updateIndicatorState() {
const hasRunningTasks = this.activePolls.size > 0;
const shouldBeVisible = hasRunningTasks || this.hasUnreadCompletedTasks;
this.indicator.classList.toggle("hidden", !shouldBeVisible);
}
// [REFACTORED for robustness]
clearCompletedTasks() {
this.tasks = this.tasks.filter((task) => this.activePolls.has(task.id));
this.hasUnreadCompletedTasks = false;
this._render();
}
// [NEW SAFETY METHOD]
_updateTaskItemInHistory(taskId, newData) {
const taskInHistory = this.tasks.find((t) => t.id === taskId);
if (taskInHistory) {
taskInHistory.data = newData;
}
}
// --- 渲染与DOM操作 ---
_render() {
this.taskListContainer.innerHTML = this.tasks.map((task) => this._createTaskItemHtml(task)).join("");
const hasTasks = this.tasks.length > 0;
this.taskListContainer.classList.toggle("hidden", !hasTasks);
this.emptyState.classList.toggle("hidden", hasTasks);
this._updateIndicatorState();
}
_createTaskItemHtml(task) {
const innerHtml = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo);
return `
${innerHtml}
`;
}
_updateTaskItemInDom(task) {
const item = this.taskListContainer.querySelector(`[data-task-id="${task.id}"]`);
if (item) {
item.innerHTML = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo);
}
}
// --- 核心面板开关逻辑 ---
openPanel() {
if (this.isAnimating || !this.panel.classList.contains("hidden")) return;
this.isAnimating = true;
this.panel.classList.remove("hidden");
this.panel.classList.add("animate-panel-in");
setTimeout(() => {
this.panel.classList.remove("animate-panel-in");
this.isAnimating = false;
this._startCountdown();
}, 150);
}
closePanel() {
if (this.isAnimating || this.panel.classList.contains("hidden")) return;
this._stopCountdown();
this.isAnimating = true;
this.panel.classList.add("animate-panel-out");
setTimeout(() => {
this.panel.classList.remove("animate-panel-out");
this.panel.classList.add("hidden");
this.isAnimating = false;
}, 50);
}
// --- [新增] 倒计时管理方法 ---
/**
* 启动或重启倒计时和进度条动画
* @private
*/
_startCountdown() {
this._stopCountdown();
this.countdownBar.classList.add("w-full", "duration-[4950ms]");
this.countdownRing.style.transition = "none";
this.countdownRing.style.strokeDashoffset = "72.26";
void this.countdownRing.offsetHeight;
this.countdownRing.style.transition = "stroke-dashoffset 4.95s linear";
this.countdownRing.style.strokeDashoffset = "0";
this.countdownTimer = setTimeout(() => {
this.closePanel();
}, 4950);
}
/**
* 停止倒计时并重置进度条
* @private
*/
_stopCountdown() {
if (this.countdownTimer) {
clearTimeout(this.countdownTimer);
this.countdownTimer = null;
}
this.countdownBar.classList.remove("w-full");
this.countdownRing.style.transition = "none";
this.countdownRing.style.strokeDashoffset = "72.26";
}
// [NEW] A central handler for any action that confirms the user has seen the panel.
_handleUserInteraction() {
this._stopCountdown();
if (this.hasUnreadCompletedTasks) {
this.hasUnreadCompletedTasks = false;
this._updateIndicatorState();
}
}
_formatTimeAgo(date) {
if (!date) return "";
const seconds = Math.floor((/* @__PURE__ */ new Date() - new Date(date)) / 1e3);
if (seconds < 2) return "\u521A\u521A";
if (seconds < 60) return `${seconds}\u79D2\u524D`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}\u5206\u949F\u524D`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}\u5C0F\u65F6\u524D`;
const days = Math.floor(hours / 24);
return `${days}\u5929\u524D`;
}
};
var ToastManager = class {
constructor() {
this.container = document.getElementById("toast-container");
if (!this.container) {
this.container = document.createElement("div");
this.container.id = "toast-container";
this.container.className = "fixed bottom-4 right-4 z-[100] w-full max-w-sm space-y-3";
document.body.appendChild(this.container);
}
this.activeToasts = /* @__PURE__ */ new Map();
}
/**
* 显示一个 Toast 通知
* @param {string} message - The message to display.
* @param {string} [type='info'] - 'info', 'success', or 'error'.
* @param {number} [duration=4000] - Duration in milliseconds.
*/
show(message, type = "info", duration = 4e3) {
const toastElement = this._createToastHtml(message, type);
this.container.appendChild(toastElement);
requestAnimationFrame(() => {
toastElement.classList.remove("opacity-0", "translate-y-2");
toastElement.classList.add("opacity-100", "translate-y-0");
});
setTimeout(() => {
toastElement.classList.remove("opacity-100", "translate-y-0");
toastElement.classList.add("opacity-0", "translate-y-2");
toastElement.addEventListener("transitionend", () => toastElement.remove(), { once: true });
}, duration);
}
// [NEW] 创建或更新一个带进度条的Toast
showProgressToast(toastId, title, message, progress) {
if (this.activeToasts.has(toastId)) {
const toastElement = this.activeToasts.get(toastId);
const messageEl = toastElement.querySelector(".toast-message");
const progressBar = toastElement.querySelector(".toast-progress-bar");
messageEl.textContent = `${message} - ${Math.round(progress)}%`;
anime({
targets: progressBar,
width: `${progress}%`,
duration: 400,
easing: "easeOutQuad"
});
} else {
const toastElement = this._createProgressToastHtml(toastId, title, message, progress);
this.container.appendChild(toastElement);
this.activeToasts.set(toastId, toastElement);
requestAnimationFrame(() => {
toastElement.classList.remove("opacity-0", "translate-x-full");
toastElement.classList.add("opacity-100", "translate-x-0");
});
}
}
// [NEW] 移除一个进度Toast
dismiss(toastId, success = null) {
return new Promise((resolve) => {
if (!this.activeToasts.has(toastId)) {
resolve();
return;
}
const toastElement = this.activeToasts.get(toastId);
const performFadeOut = () => {
toastElement.classList.remove("opacity-100", "translate-x-0");
toastElement.classList.add("opacity-0", "translate-x-full");
toastElement.addEventListener("transitionend", () => {
toastElement.remove();
this.activeToasts.delete(toastId);
resolve();
}, { once: true });
};
if (success === null) {
performFadeOut();
} else {
const iconContainer = toastElement.querySelector(".toast-icon");
const messageEl = toastElement.querySelector(".toast-message");
if (success) {
const progressBar = toastElement.querySelector(".toast-progress-bar");
messageEl.textContent = "\u5DF2\u5B8C\u6210";
anime({
targets: progressBar,
width: "100%",
duration: 300,
easing: "easeOutQuad",
complete: () => {
iconContainer.innerHTML = ``;
iconContainer.className = `toast-icon bg-green-500`;
setTimeout(performFadeOut, 900);
}
});
} else {
iconContainer.innerHTML = ``;
iconContainer.className = `toast-icon bg-red-500`;
messageEl.textContent = "\u5931\u8D25";
setTimeout(performFadeOut, 1200);
}
}
});
}
_createToastHtml(message, type) {
const icons = {
info: { class: "bg-blue-500", icon: "fa-info-circle" },
success: { class: "bg-green-500", icon: "fa-check-circle" },
error: { class: "bg-red-500", icon: "fa-exclamation-triangle" }
};
const typeInfo = icons[type] || icons.info;
const toast = document.createElement("div");
toast.className = "toast-item opacity-0 translate-y-2 transition-all duration-300 ease-out";
toast.innerHTML = `
${this._capitalizeFirstLetter(type)}
${message}
`;
return toast;
}
// [NEW] 创建带进度条Toast的HTML结构
_createProgressToastHtml(toastId, title, message, progress) {
const toast = document.createElement("div");
toast.className = "toast-item opacity-0 translate-x-full transition-all duration-300 ease-out";
toast.dataset.toastId = toastId;
toast.innerHTML = `
${title}
${message} - ${Math.round(progress)}%
`;
return toast;
}
_capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
};
var taskCenterManager = new TaskCenterManager();
var toastManager = new ToastManager();
// frontend/js/pages/dashboard.js
function init() {
console.log("[Modern Frontend] Dashboard module loaded. Future logic will execute here.");
}
// frontend/js/components/tagInput.js
var TagInput = class {
constructor(container, options = {}) {
if (!container) {
console.error("TagInput container not found.");
return;
}
this.container = container;
this.input = container.querySelector(".tag-input-new");
this.tags = [];
this.options = {
validator: /.+/,
validationMessage: "\u8F93\u5165\u683C\u5F0F\u65E0\u6548",
...options
};
this.copyBtn = document.createElement("button");
this.copyBtn.className = "tag-copy-btn";
this.copyBtn.innerHTML = '';
this.copyBtn.title = "\u590D\u5236\u6240\u6709";
this.container.appendChild(this.copyBtn);
this._initEventListeners();
}
_initEventListeners() {
this.container.addEventListener("click", (e) => {
if (e.target.closest(".tag-delete")) {
this._removeTag(e.target.closest(".tag-item"));
}
});
if (this.input) {
this.input.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === "," || e.key === " ") {
e.preventDefault();
const value = this.input.value.trim();
if (value) {
this._addTag(value);
this.input.value = "";
}
}
});
this.input.addEventListener("blur", () => {
const value = this.input.value.trim();
if (value) {
this._addTag(value);
this.input.value = "";
}
});
}
this.copyBtn.addEventListener("click", this._handleCopyAll.bind(this));
}
_addTag(raw_value) {
const value = raw_value.toLowerCase();
if (!this.options.validator.test(value)) {
console.warn(`Tag validation failed for value: "${value}". Rule: ${this.options.validator}`);
this.input.placeholder = this.options.validationMessage;
this.input.classList.add("input-error");
setTimeout(() => {
this.input.classList.remove("input-error");
this.input.placeholder = "\u6DFB\u52A0...";
}, 2e3);
return;
}
if (this.tags.includes(value)) return;
this.tags.push(value);
const tagEl = document.createElement("span");
tagEl.className = "tag-item";
tagEl.innerHTML = `${value}`;
this.container.insertBefore(tagEl, this.input);
}
// 处理复制逻辑的专用方法
_handleCopyAll() {
const tagsString = this.tags.join(",");
if (!tagsString) {
this.copyBtn.innerHTML = "\u65E0\u5185\u5BB9!";
this.copyBtn.classList.add("none");
setTimeout(() => {
this.copyBtn.innerHTML = '';
this.copyBtn.classList.remove("copied");
}, 1500);
return;
}
navigator.clipboard.writeText(tagsString).then(() => {
this.copyBtn.innerHTML = "\u5DF2\u590D\u5236!";
this.copyBtn.classList.add("copied");
setTimeout(() => {
this.copyBtn.innerHTML = '';
this.copyBtn.classList.remove("copied");
}, 2e3);
}).catch((err) => {
console.error("Could not copy text: ", err);
this.copyBtn.innerHTML = "\u5931\u8D25!";
setTimeout(() => {
this.copyBtn.innerHTML = '';
}, 2e3);
});
}
_removeTag(tagEl) {
const value = tagEl.querySelector(".tag-text").textContent;
this.tags = this.tags.filter((t) => t !== value);
tagEl.remove();
}
getValues() {
return this.tags;
}
setValues(values) {
this.container.querySelectorAll(".tag-item").forEach((el) => el.remove());
this.tags = [];
if (Array.isArray(values)) {
values.filter((value) => value).forEach((value) => this._addTag(value));
}
}
};
// frontend/js/pages/keys/requestSettingsModal.js
var RequestSettingsModal = class {
constructor({ onSave }) {
this.modalId = "request-settings-modal";
this.modal = document.getElementById(this.modalId);
this.onSave = onSave;
if (!this.modal) {
throw new Error(`Modal with id "${this.modalId}" not found.`);
}
this.elements = {
saveBtn: document.getElementById("request-settings-save-btn"),
customHeadersContainer: document.getElementById("CUSTOM_HEADERS_container"),
addCustomHeaderBtn: document.getElementById("addCustomHeaderBtn"),
streamOptimizerEnabled: document.getElementById("STREAM_OPTIMIZER_ENABLED"),
streamingSettingsPanel: document.getElementById("streaming-settings-panel"),
streamMinDelay: document.getElementById("STREAM_MIN_DELAY"),
streamMaxDelay: document.getElementById("STREAM_MAX_DELAY"),
streamShortTextThresh: document.getElementById("STREAM_SHORT_TEXT_THRESHOLD"),
streamLongTextThresh: document.getElementById("STREAM_LONG_TEXT_THRESHOLD"),
streamChunkSize: document.getElementById("STREAM_CHUNK_SIZE"),
fakeStreamEnabled: document.getElementById("FAKE_STREAM_ENABLED"),
fakeStreamInterval: document.getElementById("FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"),
toolsCodeExecutionEnabled: document.getElementById("TOOLS_CODE_EXECUTION_ENABLED"),
urlContextEnabled: document.getElementById("URL_CONTEXT_ENABLED"),
showSearchLink: document.getElementById("SHOW_SEARCH_LINK"),
showThinkingProcess: document.getElementById("SHOW_THINKING_PROCESS"),
safetySettingsContainer: document.getElementById("SAFETY_SETTINGS_container"),
addSafetySettingBtn: document.getElementById("addSafetySettingBtn"),
configOverrides: document.getElementById("group-config-overrides")
};
this._initEventListeners();
}
// --- 公共 API ---
/**
* 打開模態框並填充數據
* @param {object} data - 用於填充表單的數據
*/
open(data) {
this._populateForm(data);
modalManager.show(this.modalId);
}
/**
* 關閉模態框
*/
close() {
modalManager.hide(this.modalId);
}
// --- 內部事件與邏輯 ---
_initEventListeners() {
this.modal.addEventListener("click", (e) => {
const removeBtn = e.target.closest(".remove-btn");
if (removeBtn) {
removeBtn.parentElement.remove();
}
});
if (this.elements.addCustomHeaderBtn) {
this.elements.addCustomHeaderBtn.addEventListener("click", () => this.addCustomHeaderItem());
}
if (this.elements.addSafetySettingBtn) {
this.elements.addSafetySettingBtn.addEventListener("click", () => this.addSafetySettingItem());
}
if (this.elements.saveBtn) {
this.elements.saveBtn.addEventListener("click", this._handleSave.bind(this));
}
if (this.elements.streamOptimizerEnabled) {
this.elements.streamOptimizerEnabled.addEventListener("change", (e) => {
this._toggleStreamingPanel(e.target.checked);
});
}
const closeAction = () => {
modalManager.hide(this.modalId);
};
const closeTriggers = this.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
closeTriggers.forEach((trigger) => {
trigger.addEventListener("click", closeAction);
});
this.modal.addEventListener("click", (event) => {
if (event.target === this.modal) {
closeAction();
}
});
}
async _handleSave() {
const data = this._collectFormData();
if (this.onSave) {
try {
if (this.elements.saveBtn) {
this.elements.saveBtn.disabled = true;
this.elements.saveBtn.textContent = "Saving...";
}
await this.onSave(data);
this.close();
} catch (error) {
console.error("Failed to save request settings:", error);
alert(`\u4FDD\u5B58\u5931\u6557: ${error.message}`);
} finally {
if (this.elements.saveBtn) {
this.elements.saveBtn.disabled = false;
this.elements.saveBtn.textContent = "Save Changes";
}
}
}
}
// --- 所有表單處理輔助方法 ---
_populateForm(data = {}) {
const isStreamOptimizerEnabled = !!data.stream_optimizer_enabled;
this._setToggle(this.elements.streamOptimizerEnabled, isStreamOptimizerEnabled);
this._toggleStreamingPanel(isStreamOptimizerEnabled);
this._setValue(this.elements.streamMinDelay, data.stream_min_delay);
this._setValue(this.elements.streamMaxDelay, data.stream_max_delay);
this._setValue(this.elements.streamShortTextThresh, data.stream_short_text_threshold);
this._setValue(this.elements.streamLongTextThresh, data.stream_long_text_threshold);
this._setValue(this.elements.streamChunkSize, data.stream_chunk_size);
this._setToggle(this.elements.fakeStreamEnabled, data.fake_stream_enabled);
this._setValue(this.elements.fakeStreamInterval, data.fake_stream_empty_data_interval_seconds);
this._setToggle(this.elements.toolsCodeExecutionEnabled, data.tools_code_execution_enabled);
this._setToggle(this.elements.urlContextEnabled, data.url_context_enabled);
this._setToggle(this.elements.showSearchLink, data.show_search_link);
this._setToggle(this.elements.showThinkingProcess, data.show_thinking_process);
this._setValue(this.elements.configOverrides, data.config_overrides);
this._populateKVItems(this.elements.customHeadersContainer, data.custom_headers, this.addCustomHeaderItem.bind(this));
this._clearContainer(this.elements.safetySettingsContainer);
if (data.safety_settings && typeof data.safety_settings === "object") {
for (const [key, value] of Object.entries(data.safety_settings)) {
this.addSafetySettingItem(key, value);
}
}
}
/**
* Collects all data from the form fields and returns it as an object.
* @returns {object} The collected request configuration data.
*/
collectFormData() {
return {
// Simple Toggles & Inputs
stream_optimizer_enabled: this.elements.streamOptimizerEnabled.checked,
stream_min_delay: parseInt(this.elements.streamMinDelay.value, 10),
stream_max_delay: parseInt(this.elements.streamMaxDelay.value, 10),
stream_short_text_threshold: parseInt(this.elements.streamShortTextThresh.value, 10),
stream_long_text_threshold: parseInt(this.elements.streamLongTextThresh.value, 10),
stream_chunk_size: parseInt(this.elements.streamChunkSize.value, 10),
fake_stream_enabled: this.elements.fakeStreamEnabled.checked,
fake_stream_empty_data_interval_seconds: parseInt(this.elements.fakeStreamInterval.value, 10),
tools_code_execution_enabled: this.elements.toolsCodeExecutionEnabled.checked,
url_context_enabled: this.elements.urlContextEnabled.checked,
show_search_link: this.elements.showSearchLink.checked,
show_thinking_process: this.elements.showThinkingProcess.checked,
config_overrides: this.elements.configOverrides.value,
// Dynamic & Complex Fields
custom_headers: this._collectKVItems(this.elements.customHeadersContainer),
safety_settings: this._collectSafetySettings(this.elements.safetySettingsContainer)
// TODO: Collect from Tag Inputs
// image_models: this.imageModelsInput.getValues(),
};
}
// 控制流式面板显示/隐藏的辅助函数
_toggleStreamingPanel(is_enabled) {
if (this.elements.streamingSettingsPanel) {
if (is_enabled) {
this.elements.streamingSettingsPanel.classList.remove("hidden");
} else {
this.elements.streamingSettingsPanel.classList.add("hidden");
}
}
}
/**
* Adds a new key-value pair item for Custom Headers.
* @param {string} [key=''] - The initial key.
* @param {string} [value=''] - The initial value.
*/
addCustomHeaderItem(key = "", value = "") {
const container = this.elements.customHeadersContainer;
const item = document.createElement("div");
item.className = "dynamic-kv-item";
item.innerHTML = `
`;
container.appendChild(item);
}
/**
* Adds a new item for Safety Settings.
* @param {string} [category=''] - The initial category.
* @param {string} [threshold=''] - The initial threshold.
*/
addSafetySettingItem(category = "", threshold = "") {
const container = this.elements.safetySettingsContainer;
const item = document.createElement("div");
item.className = "safety-setting-item flex items-center gap-x-2";
const harmCategories = [
"HARM_CATEGORY_HARASSMENT",
"HARM_CATEGORY_HATE_SPEECH",
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
"HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_CIVIC_INTEGRITY"
];
const harmThresholds = [
"BLOCK_OFF",
"BLOCK_NONE",
"BLOCK_LOW_AND_ABOVE",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_ONLY_HIGH"
];
const categorySelect = document.createElement("select");
categorySelect.className = "modal-input flex-grow";
harmCategories.forEach((cat) => {
const option = new Option(cat.replace("HARM_CATEGORY_", ""), cat);
if (cat === category) option.selected = true;
categorySelect.add(option);
});
const thresholdSelect = document.createElement("select");
thresholdSelect.className = "modal-input w-48";
harmThresholds.forEach((thr) => {
const option = new Option(thr.replace("BLOCK_", "").replace("_AND_ABOVE", "+"), thr);
if (thr === threshold) option.selected = true;
thresholdSelect.add(option);
});
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.className = "remove-btn text-zinc-400 hover:text-red-500 transition-colors";
removeButton.innerHTML = ``;
item.appendChild(categorySelect);
item.appendChild(thresholdSelect);
item.appendChild(removeButton);
container.appendChild(item);
}
// --- Private Helper Methods for Form Handling ---
_setValue(element, value) {
if (element && value !== null && value !== void 0) {
element.value = value;
}
}
_setToggle(element, value) {
if (element) {
element.checked = !!value;
}
}
_clearContainer(container) {
if (container) {
const firstChild = container.firstElementChild;
const isTemplate = firstChild && (firstChild.tagName === "TEMPLATE" || firstChild.id === "kv-item-header");
let child = isTemplate ? firstChild.nextElementSibling : container.firstElementChild;
while (child) {
const next = child.nextElementSibling;
child.remove();
child = next;
}
}
}
_populateKVItems(container, items, addItemFn) {
this._clearContainer(container);
if (items && typeof items === "object") {
for (const [key, value] of Object.entries(items)) {
addItemFn(key, value);
}
}
}
_collectKVItems(container) {
const items = {};
container.querySelectorAll(".dynamic-kv-item").forEach((item) => {
const keyEl = item.querySelector(".dynamic-kv-key");
const valueEl = item.querySelector(".dynamic-kv-value");
if (keyEl && valueEl && keyEl.value) {
items[keyEl.value] = valueEl.value;
}
});
return items;
}
_collectSafetySettings(container) {
const items = {};
container.querySelectorAll(".safety-setting-item").forEach((item) => {
const categorySelect = item.querySelector("select:first-child");
const thresholdSelect = item.querySelector("select:last-of-type");
if (categorySelect && thresholdSelect && categorySelect.value) {
items[categorySelect.value] = thresholdSelect.value;
}
});
return items;
}
};
// frontend/js/services/api.js
var APIClientError = class extends Error {
constructor(message, status, code, rawMessageFromServer) {
super(message);
this.name = "APIClientError";
this.status = status;
this.code = code;
this.rawMessageFromServer = rawMessageFromServer;
}
};
var apiPromiseCache = /* @__PURE__ */ new Map();
async function apiFetch(url, options = {}) {
const isGetRequest = !options.method || options.method.toUpperCase() === "GET";
const cacheKey = isGetRequest && !options.noCache ? url : null;
if (cacheKey && apiPromiseCache.has(cacheKey)) {
return apiPromiseCache.get(cacheKey);
}
const token = localStorage.getItem("bearerToken");
const headers = {
"Content-Type": "application/json",
...options.headers
};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const requestPromise = (async () => {
try {
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
if (cacheKey) apiPromiseCache.delete(cacheKey);
localStorage.removeItem("bearerToken");
if (window.location.pathname !== "/login") {
window.location.href = "/login?error=\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u3002";
}
throw new APIClientError("Unauthorized", 401, "UNAUTHORIZED", "Session expired or token is invalid.");
}
if (!response.ok) {
let errorData = null;
let rawMessage = "";
try {
rawMessage = await response.text();
if (rawMessage) {
errorData = JSON.parse(rawMessage);
}
} catch (e) {
errorData = { error: { code: "UNKNOWN_FORMAT", message: rawMessage || response.statusText } };
}
const code = errorData?.error?.code || "UNKNOWN_ERROR";
const messageFromServer = errorData?.error?.message || rawMessage || "No message provided by server.";
const error = new APIClientError(
`API request failed: ${response.status}`,
response.status,
code,
messageFromServer
);
throw error;
}
return response;
} catch (error) {
if (cacheKey) apiPromiseCache.delete(cacheKey);
throw error;
}
})();
if (cacheKey) {
apiPromiseCache.set(cacheKey, requestPromise);
}
return requestPromise;
}
async function apiFetchJson(url, options = {}) {
try {
const response = await apiFetch(url, options);
const clonedResponse = response.clone();
const jsonData = await clonedResponse.json();
return jsonData;
} catch (error) {
throw error;
}
}
// frontend/js/components/apiKeyManager.js
var ApiKeyManager = class {
constructor() {
}
// [新增] 开始一个向指定分组添加Keys的异步任务
/**
* Starts a task to add multiple API keys to a specific group.
* @param {number} groupId - The ID of the group.
* @param {string} keysText - A string of keys, separated by newlines.
* @returns {Promise