// 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/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(); export { CustomSelect, taskCenterManager, toastManager };