((resolve) => httpServer.close(() => resolve()));
+ process.exit(0);
+ };
+
+ process.on("SIGINT", shutdownHandler);
+ process.on("SIGTERM", shutdownHandler);
+ }
// 創建MCP服務器
const server = new Server(
diff --git a/src/public/index.html b/src/public/index.html
new file mode 100644
index 0000000..72836d1
--- /dev/null
+++ b/src/public/index.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+ Shrimp Task Manager
+
+
+
+
+
+ Shrimp Task Manager
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/public/locales/en.json b/src/public/locales/en.json
new file mode 100644
index 0000000..7d75188
--- /dev/null
+++ b/src/public/locales/en.json
@@ -0,0 +1,55 @@
+{
+ "app_title": "Shrimp Task Manager",
+ "status_indicator_online": "ONLINE",
+ "dependency_view_title": "Dependency View",
+ "dependency_graph_placeholder": "Dependency relationship for all tasks",
+ "task_list_title": "Task List",
+ "search_placeholder": "Search tasks...",
+ "sort_option_date_desc": "Creation Time (New-Old)",
+ "sort_option_date_asc": "Creation Time (Old-New)",
+ "sort_option_name_asc": "Name (A-Z)",
+ "sort_option_name_desc": "Name (Z-A)",
+ "sort_option_status": "Status",
+ "status_filter_all": "All Statuses",
+ "status_filter_pending": "Pending",
+ "status_filter_in_progress": "In Progress",
+ "status_filter_completed": "Completed",
+ "task_list_loading": "Loading...",
+ "task_list_empty": "No matching tasks",
+ "task_details_title": "Task Details",
+ "task_details_placeholder": "Select a task to view details",
+ "footer_copyright": "© 2023 Shrimp Task Manager - Current time: ",
+
+ "status_pending": "Pending",
+ "status_in_progress": "In Progress",
+ "status_completed": "Completed",
+
+ "task_detail_status_label": "Status:",
+ "task_detail_summary_title": "Completion Summary",
+ "task_detail_description_title": "Task Description",
+ "task_detail_implementation_guide_title": "Implementation Guide",
+ "task_detail_verification_criteria_title": "Verification Criteria",
+ "task_detail_dependencies_title": "Dependencies (Prerequisites)",
+ "task_detail_related_files_title": "Related Files",
+ "task_detail_notes_title": "Notes",
+ "task_detail_no_summary": "No summary provided.",
+ "task_detail_no_description": "No description",
+ "task_detail_no_implementation_guide": "No implementation guide",
+ "task_detail_no_verification_criteria": "No verification criteria",
+ "task_detail_no_dependencies": "No dependencies",
+ "task_detail_no_related_files": "No related files",
+ "task_detail_no_notes": "No notes",
+ "task_detail_unknown_dependency": "Unknown Task",
+
+ "progress_completed": "Completed",
+ "progress_in_progress": "In Progress",
+ "progress_pending": "Pending",
+ "progress_total": "Total",
+
+ "global_analysis_title": "Goal",
+
+ "error_loading_tasks": "Failed to load tasks: {message}",
+ "error_updating_tasks": "Failed to update tasks: {message}",
+ "error_loading_graph": "Failed to load dependency graph",
+ "error_task_not_found": "Task not found"
+}
diff --git a/src/public/locales/zh-TW.json b/src/public/locales/zh-TW.json
new file mode 100644
index 0000000..d9b99bd
--- /dev/null
+++ b/src/public/locales/zh-TW.json
@@ -0,0 +1,55 @@
+{
+ "app_title": "Shrimp Task Manager",
+ "status_indicator_online": "在線",
+ "dependency_view_title": "依賴關係",
+ "dependency_graph_placeholder": "所有任務的依賴關係",
+ "task_list_title": "任務列表",
+ "search_placeholder": "搜索任務...",
+ "sort_option_date_desc": "創建時間 (新-舊)",
+ "sort_option_date_asc": "創建時間 (舊-新)",
+ "sort_option_name_asc": "名稱 (A-Z)",
+ "sort_option_name_desc": "名稱 (Z-A)",
+ "sort_option_status": "狀態",
+ "status_filter_all": "所有狀態",
+ "status_filter_pending": "等待中",
+ "status_filter_in_progress": "進行中",
+ "status_filter_completed": "已完成",
+ "task_list_loading": "載入中...",
+ "task_list_empty": "沒有符合條件的任務",
+ "task_details_title": "任務詳情",
+ "task_details_placeholder": "選擇一個任務查看詳情",
+ "footer_copyright": "© 2023 Shrimp Task Manager - 當前時間: ",
+
+ "status_pending": "等待中",
+ "status_in_progress": "進行中",
+ "status_completed": "已完成",
+
+ "task_detail_status_label": "狀態:",
+ "task_detail_summary_title": "完成摘要",
+ "task_detail_description_title": "任務描述",
+ "task_detail_implementation_guide_title": "實現指南",
+ "task_detail_verification_criteria_title": "驗證標準",
+ "task_detail_dependencies_title": "依賴項 (前置任務)",
+ "task_detail_related_files_title": "相關文件",
+ "task_detail_notes_title": "備註",
+ "task_detail_no_summary": "沒有提供摘要。",
+ "task_detail_no_description": "無描述",
+ "task_detail_no_implementation_guide": "無實現指南",
+ "task_detail_no_verification_criteria": "無驗證標準",
+ "task_detail_no_dependencies": "無依賴項",
+ "task_detail_no_related_files": "無相關文件",
+ "task_detail_no_notes": "無備註",
+ "task_detail_unknown_dependency": "未知任務",
+
+ "progress_completed": "已完成",
+ "progress_in_progress": "進行中",
+ "progress_pending": "待處理",
+ "progress_total": "總計",
+
+ "global_analysis_title": "目標",
+
+ "error_loading_tasks": "載入任務失敗: {message}",
+ "error_updating_tasks": "更新任務失敗: {message}",
+ "error_loading_graph": "載入依賴圖失敗",
+ "error_task_not_found": "任務未找到"
+}
diff --git a/src/public/script.js b/src/public/script.js
new file mode 100644
index 0000000..80b0666
--- /dev/null
+++ b/src/public/script.js
@@ -0,0 +1,1186 @@
+// 全局變量
+let tasks = [];
+let selectedTaskId = null;
+let searchTerm = "";
+let sortOption = "date-asc";
+let globalAnalysisResult = null; // 新增:儲存全局分析結果
+let svg, g, simulation; // << 修改:定義 D3 相關變量
+
+// 新增:i18n 全局變量
+let currentLang = "en"; // 預設語言
+let translations = {}; // 儲存加載的翻譯
+
+// DOM元素
+const taskListElement = document.getElementById("task-list");
+const taskDetailsContent = document.getElementById("task-details-content");
+const statusFilter = document.getElementById("status-filter");
+const currentTimeElement = document.getElementById("current-time");
+const progressIndicator = document.getElementById("progress-indicator");
+const progressCompleted = document.getElementById("progress-completed");
+const progressInProgress = document.getElementById("progress-in-progress");
+const progressPending = document.getElementById("progress-pending");
+const progressLabels = document.getElementById("progress-labels");
+const dependencyGraphElement = document.getElementById("dependency-graph");
+const globalAnalysisResultElement = document.getElementById(
+ "global-analysis-result"
+); // 假設 HTML 中有這個元素
+const langSwitcher = document.getElementById("lang-switcher"); // << 新增:獲取切換器元素
+
+// 初始化
+document.addEventListener("DOMContentLoaded", () => {
+ // fetchTasks(); // 將由 initI18n() 觸發
+ initI18n(); // << 新增:初始化 i18n
+ updateCurrentTime();
+ setInterval(updateCurrentTime, 1000);
+
+ // 事件監聽器
+ // statusFilter.addEventListener("change", renderTasks); // 將由 changeLanguage 觸發或在 applyTranslations 後觸發
+ if (statusFilter) {
+ statusFilter.addEventListener("change", renderTasks);
+ }
+
+ // 新增:搜索和排序事件監聽
+ const searchInput = document.getElementById("search-input");
+ const sortOptions = document.getElementById("sort-options");
+
+ if (searchInput) {
+ searchInput.addEventListener("input", (e) => {
+ searchTerm = e.target.value.toLowerCase();
+ renderTasks();
+ });
+ }
+
+ if (sortOptions) {
+ sortOptions.addEventListener("change", (e) => {
+ sortOption = e.target.value;
+ renderTasks();
+ });
+ }
+
+ // 新增:設置 SSE 連接
+ setupSSE();
+
+ // 新增:語言切換器事件監聽
+ if (langSwitcher) {
+ langSwitcher.addEventListener("change", (e) =>
+ changeLanguage(e.target.value)
+ );
+ }
+});
+
+// 新增:i18n 核心函數
+// 1. 語言檢測 (localStorage > navigator.language > 'en')
+function detectLanguage() {
+ const savedLang = localStorage.getItem("lang");
+ if (savedLang && ["en", "zh-TW"].includes(savedLang)) {
+ // 確保保存的是有效語言
+ return savedLang;
+ }
+ // 檢查瀏覽器語言
+ const browserLang = navigator.language || navigator.userLanguage;
+ if (browserLang) {
+ if (browserLang.toLowerCase().startsWith("zh-tw")) return "zh-TW";
+ if (browserLang.toLowerCase().startsWith("zh")) return "zh-TW"; // 簡體也先 fallback 到繁體
+ if (browserLang.toLowerCase().startsWith("en")) return "en";
+ }
+ return "en"; // 預設
+}
+
+// 2. 異步加載翻譯文件
+async function loadTranslations(lang) {
+ try {
+ const response = await fetch(`/locales/${lang}.json`);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to load ${lang}.json, status: ${response.status}`
+ );
+ }
+ translations = await response.json();
+ console.log(`Translations loaded for ${lang}`);
+ } catch (error) {
+ console.error("Error loading translations:", error);
+ if (lang !== "en") {
+ console.warn(`Falling back to English translations.`);
+ await loadTranslations("en"); // Fallback to English
+ } else {
+ translations = {}; // Clear translations if even English fails
+ // Maybe display a more persistent error message?
+ alert("Critical error: Could not load language files.");
+ }
+ }
+}
+
+// 3. 翻譯函數
+function translate(key, replacements = {}) {
+ let translated = translations[key] || key; // Fallback to key itself
+ // 簡單的佔位符替換(例如 {message})
+ for (const placeholder in replacements) {
+ translated = translated.replace(
+ `{${placeholder}}`,
+ replacements[placeholder]
+ );
+ }
+ return translated;
+}
+
+// 4. 應用翻譯到 DOM (處理 textContent, placeholder, title)
+function applyTranslations() {
+ console.log("Applying translations for:", currentLang);
+ document.querySelectorAll("[data-i18n-key]").forEach((el) => {
+ const key = el.dataset.i18nKey;
+ const translatedText = translate(key);
+
+ // 優先處理特定屬性
+ if (el.hasAttribute("placeholder")) {
+ el.placeholder = translatedText;
+ } else if (el.hasAttribute("title")) {
+ el.title = translatedText;
+ } else if (el.tagName === "OPTION") {
+ el.textContent = translatedText;
+ // 如果需要,也可以翻譯 value,但通常不需要
+ } else {
+ // 對於大多數元素,設置 textContent
+ el.textContent = translatedText;
+ }
+ });
+ // 手動更新沒有 data-key 的元素(如果有的話)
+ // 例如,如果 footer 時間格式需要本地化,可以在這裡處理
+ // updateCurrentTime(); // 確保時間格式也可能更新(如果需要)
+}
+
+// 5. 初始化 i18n
+async function initI18n() {
+ currentLang = detectLanguage();
+ console.log(`Initializing i18n with language: ${currentLang}`);
+ localStorage.setItem("lang", currentLang); // 確保 lang 被保存
+ // << 新增:設置切換器的初始值 >>
+ if (langSwitcher) {
+ langSwitcher.value = currentLang;
+ }
+ await loadTranslations(currentLang);
+ applyTranslations();
+ await fetchTasks();
+}
+
+// 新增:語言切換函數
+function changeLanguage(lang) {
+ if (!lang || !["en", "zh-TW"].includes(lang)) {
+ console.warn(`Invalid language selected: ${lang}. Defaulting to English.`);
+ lang = "en";
+ }
+ currentLang = lang;
+ localStorage.setItem("lang", lang);
+ console.log(`Changing language to: ${currentLang}`);
+ loadTranslations(currentLang)
+ .then(() => {
+ console.log("Translations reloaded, applying...");
+ applyTranslations();
+ console.log("Re-rendering components...");
+ // 重新渲染需要翻譯的組件
+ renderTasks();
+ if (selectedTaskId) {
+ const task = tasks.find((t) => t.id === selectedTaskId);
+ if (task) {
+ selectTask(selectedTaskId); // 確保傳遞 ID,讓 selectTask 重新查找並渲染
+ } else {
+ // 如果選中的任務已不存在,清除詳情
+ taskDetailsContent.innerHTML = `${translate(
+ "task_details_placeholder"
+ )}
`;
+ selectedTaskId = null;
+ highlightNode(null);
+ }
+ } else {
+ // 如果沒有任務被選中,確保詳情面板顯示 placeholder
+ taskDetailsContent.innerHTML = `${translate(
+ "task_details_placeholder"
+ )}
`;
+ }
+ renderDependencyGraph(); // 重新渲染圖表(可能包含 placeholder)
+ updateProgressIndicator(); // 重新渲染進度條(包含標籤)
+ renderGlobalAnalysisResult(); // 重新渲染全局分析(標題)
+ // 確保下拉菜單的值與當前語言一致
+ if (langSwitcher) langSwitcher.value = currentLang;
+ console.log("Language change complete.");
+ })
+ .catch((error) => {
+ console.error("Error changing language:", error);
+ // 可以添加用戶反饋,例如顯示錯誤消息
+ showTemporaryError("Failed to change language. Please try again."); // Need translation key
+ });
+}
+// --- i18n 核心函數結束 ---
+
+// 獲取任務數據
+async function fetchTasks() {
+ try {
+ // 初始載入時顯示 loading (現在使用翻譯)
+ if (tasks.length === 0) {
+ taskListElement.innerHTML = `${translate(
+ "task_list_loading"
+ )}
`;
+ }
+
+ const response = await fetch("/api/tasks");
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const newTasks = data.tasks || [];
+
+ // 提取全局分析結果 (找第一個非空的)
+ let foundAnalysisResult = null;
+ for (const task of newTasks) {
+ if (task.analysisResult) {
+ foundAnalysisResult = task.analysisResult;
+ break; // 找到一個就夠了
+ }
+ }
+ // 只有當找到的結果與當前儲存的不同時才更新
+ if (foundAnalysisResult !== globalAnalysisResult) {
+ globalAnalysisResult = foundAnalysisResult;
+ renderGlobalAnalysisResult(); // 更新顯示
+ }
+
+ // --- 智慧更新邏輯 (初步 - 仍需改進以避免閃爍) ---
+ // 簡單地比較任務數量或標識符來決定是否重新渲染
+ // 理想情況下應比較每個任務的內容並進行 DOM 更新
+ const tasksChanged = didTasksChange(tasks, newTasks);
+
+ if (tasksChanged) {
+ tasks = newTasks; // 更新全局任務列表
+ console.log("Tasks updated via fetch, re-rendering...");
+ renderTasks();
+ updateProgressIndicator();
+ renderDependencyGraph(); // 更新圖表
+ } else {
+ console.log(
+ "No significant task changes detected, skipping full re-render."
+ );
+ // 如果不需要重新渲染列表,可能只需要更新進度條
+ updateProgressIndicator();
+ // 考慮是否需要更新圖表(如果狀態可能改變)
+ // renderDependencyGraph(); // 暫時註釋掉,除非狀態變化很關鍵
+ }
+
+ // *** 移除 setTimeout 輪詢 ***
+ // setTimeout(fetchTasks, 30000);
+ } catch (error) {
+ console.error("Error fetching tasks:", error);
+ // 避免覆蓋現有列表,除非是初始載入失敗
+ if (tasks.length === 0) {
+ taskListElement.innerHTML = `${translate(
+ "error_loading_tasks",
+ { message: error.message }
+ )}
`;
+ if (progressIndicator) progressIndicator.style.display = "none";
+ if (dependencyGraphElement)
+ dependencyGraphElement.innerHTML = `${translate(
+ "error_loading_graph"
+ )}
`;
+ } else {
+ showTemporaryError(
+ translate("error_updating_tasks", { message: error.message })
+ );
+ }
+ }
+}
+
+// 新增:設置 Server-Sent Events 連接
+function setupSSE() {
+ console.log("Setting up SSE connection to /api/tasks/stream");
+ const evtSource = new EventSource("/api/tasks/stream");
+
+ evtSource.onmessage = function (event) {
+ console.log("SSE message received:", event.data);
+ // 可以根據 event.data 內容做更複雜的判斷,目前只要收到消息就更新
+ };
+
+ evtSource.addEventListener("update", function (event) {
+ console.log("SSE 'update' event received:", event.data);
+ // 收到更新事件,重新獲取任務列表
+ fetchTasks();
+ });
+
+ evtSource.onerror = function (err) {
+ console.error("EventSource failed:", err);
+ // 可以實現重連邏輯
+ evtSource.close(); // 關閉錯誤的連接
+ // 延遲一段時間後嘗試重新連接
+ setTimeout(setupSSE, 5000); // 5秒後重試
+ };
+
+ evtSource.onopen = function () {
+ console.log("SSE connection opened.");
+ };
+}
+
+// 新增:比較任務列表是否有變化的輔助函數 (最全面版)
+function didTasksChange(oldTasks, newTasks) {
+ if (!oldTasks || !newTasks) return true; // Handle initial load or error states
+
+ if (oldTasks.length !== newTasks.length) {
+ console.log("Task length changed.");
+ return true; // Length change definitely needs update
+ }
+
+ const oldTaskMap = new Map(oldTasks.map((task) => [task.id, task]));
+ const newTaskIds = new Set(newTasks.map((task) => task.id)); // For checking removed tasks
+
+ // Check for removed tasks first
+ for (const oldTask of oldTasks) {
+ if (!newTaskIds.has(oldTask.id)) {
+ console.log(`Task removed: ${oldTask.id}`);
+ return true;
+ }
+ }
+
+ // Check for new or modified tasks
+ for (const newTask of newTasks) {
+ const oldTask = oldTaskMap.get(newTask.id);
+ if (!oldTask) {
+ console.log(`New task found: ${newTask.id}`);
+ return true; // New task ID found
+ }
+
+ // Compare relevant fields
+ const fieldsToCompare = [
+ "name",
+ "description",
+ "status",
+ "notes",
+ "implementationGuide",
+ "verificationCriteria",
+ "summary",
+ ];
+
+ for (const field of fieldsToCompare) {
+ if (oldTask[field] !== newTask[field]) {
+ // Handle null/undefined comparisons carefully if needed
+ // e.g., !(oldTask[field] == null && newTask[field] == null) checks if one is null/undefined and the other isn't
+ if (
+ !(oldTask[field] === null && newTask[field] === null) &&
+ !(oldTask[field] === undefined && newTask[field] === undefined)
+ ) {
+ console.log(`Task ${newTask.id} changed field: ${field}`);
+ return true;
+ }
+ }
+ }
+
+ // Compare dependencies (array of strings or objects)
+ if (!compareDependencies(oldTask.dependencies, newTask.dependencies)) {
+ console.log(`Task ${newTask.id} changed field: dependencies`);
+ return true;
+ }
+
+ // Compare relatedFiles (array of objects) - simple length check first
+ if (!compareRelatedFiles(oldTask.relatedFiles, newTask.relatedFiles)) {
+ console.log(`Task ${newTask.id} changed field: relatedFiles`);
+ return true;
+ }
+
+ // Optional: Compare updatedAt as a final check if other fields seem identical
+ if (oldTask.updatedAt?.toString() !== newTask.updatedAt?.toString()) {
+ console.log(`Task ${newTask.id} changed field: updatedAt (fallback)`);
+ return true;
+ }
+ }
+
+ return false; // No significant changes detected
+}
+
+// Helper function to compare dependency arrays
+function compareDependencies(deps1, deps2) {
+ const arr1 = deps1 || [];
+ const arr2 = deps2 || [];
+
+ if (arr1.length !== arr2.length) return false;
+
+ // Extract IDs whether they are strings or objects {taskId: string}
+ const ids1 = new Set(
+ arr1.map((dep) =>
+ typeof dep === "object" && dep !== null ? dep.taskId : dep
+ )
+ );
+ const ids2 = new Set(
+ arr2.map((dep) =>
+ typeof dep === "object" && dep !== null ? dep.taskId : dep
+ )
+ );
+
+ if (ids1.size !== ids2.size) return false; // Different number of unique deps
+ for (const id of ids1) {
+ if (!ids2.has(id)) return false;
+ }
+ return true;
+}
+
+// Helper function to compare relatedFiles arrays (can be simple or complex)
+function compareRelatedFiles(files1, files2) {
+ const arr1 = files1 || [];
+ const arr2 = files2 || [];
+
+ if (arr1.length !== arr2.length) return false;
+
+ // Simple comparison: check if paths and types are the same in the same order
+ // For a more robust check, convert to Sets of strings like `path|type` or do deep object comparison
+ for (let i = 0; i < arr1.length; i++) {
+ if (arr1[i].path !== arr2[i].path || arr1[i].type !== arr2[i].type) {
+ return false;
+ }
+ // Add more field comparisons if needed (description, lines, etc.)
+ // if (arr1[i].description !== arr2[i].description) return false;
+ }
+ return true;
+}
+
+// 新增:顯示臨時錯誤訊息的函數
+function showTemporaryError(message) {
+ const errorElement = document.createElement("div");
+ errorElement.className = "temporary-error";
+ errorElement.textContent = message; // 保持消息本身
+ document.body.appendChild(errorElement);
+ setTimeout(() => {
+ errorElement.remove();
+ }, 3000); // 顯示 3 秒
+}
+
+// 渲染任務列表 - *** 需要進一步優化以實現智慧更新 ***
+function renderTasks() {
+ console.log("Rendering tasks..."); // 添加日誌
+ const filterValue = statusFilter.value;
+
+ let filteredTasks = tasks;
+ if (filterValue !== "all") {
+ filteredTasks = filteredTasks.filter((task) => task.status === filterValue);
+ }
+
+ if (searchTerm) {
+ const lowerCaseSearchTerm = searchTerm.toLowerCase();
+ filteredTasks = filteredTasks.filter(
+ (task) =>
+ (task.name && task.name.toLowerCase().includes(lowerCaseSearchTerm)) ||
+ (task.description &&
+ task.description.toLowerCase().includes(lowerCaseSearchTerm))
+ );
+ }
+
+ filteredTasks.sort((a, b) => {
+ switch (sortOption) {
+ case "name-asc":
+ return (a.name || "").localeCompare(b.name || "");
+ case "name-desc":
+ return (b.name || "").localeCompare(a.name || "");
+ case "status":
+ const statusOrder = { pending: 1, in_progress: 2, completed: 3 };
+ return (statusOrder[a.status] || 0) - (statusOrder[b.status] || 0);
+ case "date-asc":
+ return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
+ case "date-desc":
+ default:
+ return new Date(b.createdAt || 0) - new Date(a.createdAt || 0);
+ }
+ });
+
+ // --- 簡單粗暴的替換 (會導致閃爍) ---
+ // TODO: 實現 DOM Diffing 或更智慧的更新策略
+ if (filteredTasks.length === 0) {
+ taskListElement.innerHTML = `${translate(
+ "task_list_empty"
+ )}
`;
+ } else {
+ taskListElement.innerHTML = filteredTasks
+ .map(
+ (task) => `
+
+
${task.name}
+
+ ${getStatusText(task.status)}
+
+
+ `
+ )
+ .join("");
+ }
+ // --- 結束簡單粗暴的替換 ---
+
+ // 重新應用選中狀態
+ if (selectedTaskId) {
+ const taskExists = tasks.some((t) => t.id === selectedTaskId);
+ if (taskExists) {
+ const selectedElement = document.querySelector(
+ `.task-item[data-id="${selectedTaskId}"]`
+ );
+ if (selectedElement) {
+ selectedElement.classList.add("selected");
+ }
+ } else {
+ // 如果選中的任務在新的列表中不存在了,清除選擇
+ console.log(
+ `Selected task ${selectedTaskId} no longer exists, clearing selection.`
+ );
+ selectedTaskId = null;
+ taskDetailsContent.innerHTML = `${translate(
+ "task_details_placeholder"
+ )}
`;
+ highlightNode(null); // 清除圖表高亮
+ }
+ }
+}
+
+// 選擇任務
+function selectTask(taskId) {
+ // 清除舊的選中狀態和高亮
+ if (selectedTaskId) {
+ const previousElement = document.querySelector(
+ `.task-item[data-id="${selectedTaskId}"]`
+ );
+ if (previousElement) {
+ previousElement.classList.remove("selected");
+ }
+ }
+
+ // 如果再次點擊同一個任務,則取消選中
+ if (selectedTaskId === taskId) {
+ selectedTaskId = null;
+ taskDetailsContent.innerHTML = `${translate(
+ "task_details_placeholder"
+ )}
`;
+ highlightNode(null); // 取消高亮
+ return;
+ }
+
+ selectedTaskId = taskId;
+
+ // 添加新的選中狀態
+ const selectedElement = document.querySelector(
+ `.task-item[data-id="${taskId}"]`
+ );
+ if (selectedElement) {
+ selectedElement.classList.add("selected");
+ }
+
+ // 獲取並顯示任務詳情
+ const task = tasks.find((t) => t.id === taskId);
+
+ if (!task) {
+ taskDetailsContent.innerHTML = `${translate(
+ "error_task_not_found"
+ )}
`;
+ return;
+ }
+
+ // --- 安全地填充任務詳情 ---
+ // 1. 創建基本骨架 (使用 innerHTML,但將動態內容替換為帶 ID 的空元素)
+ taskDetailsContent.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // 2. 獲取對應元素並使用 textContent 安全地填充內容
+ const detailName = document.getElementById("detail-name");
+ const detailStatus = document.getElementById("detail-status");
+ const detailDescription = document.getElementById("detail-description");
+ const detailImplementationGuide = document.getElementById(
+ "detail-implementation-guide"
+ );
+ const detailVerificationCriteria = document.getElementById(
+ "detail-verification-criteria"
+ );
+ // 新增:獲取 Summary 相關元素
+ const detailSummarySection = document.getElementById(
+ "detail-summary-section"
+ );
+ const detailSummary = document.getElementById("detail-summary");
+ const detailNotes = document.getElementById("detail-notes");
+ const detailDependencies = document.getElementById("detail-dependencies");
+ const detailRelatedFiles = document.getElementById("detail-related-files");
+
+ if (detailName) detailName.textContent = task.name;
+ if (detailStatus) {
+ detailStatus.textContent = getStatusText(task.status);
+ detailStatus.className = `task-status status-${task.status.replace(
+ "_",
+ "-"
+ )}`;
+ }
+ if (detailDescription)
+ detailDescription.textContent =
+ task.description || translate("task_detail_no_description");
+ if (detailImplementationGuide)
+ detailImplementationGuide.textContent =
+ task.implementationGuide ||
+ translate("task_detail_no_implementation_guide");
+ if (detailVerificationCriteria)
+ detailVerificationCriteria.textContent =
+ task.verificationCriteria ||
+ translate("task_detail_no_verification_criteria");
+
+ // 新增:填充 Summary (如果存在且已完成)
+ if (task.summary && detailSummarySection && detailSummary) {
+ detailSummary.textContent = task.summary;
+ detailSummarySection.style.display = "block"; // 顯示區塊
+ } else if (detailSummarySection) {
+ detailSummarySection.style.display = "none"; // 隱藏區塊
+ }
+
+ if (detailNotes)
+ detailNotes.textContent = task.notes || translate("task_detail_no_notes");
+
+ // 3. 動態生成依賴項和相關文件 (這些可以包含安全的 HTML 結構如 span)
+ if (detailDependencies) {
+ const dependenciesHtml =
+ task.dependencies && task.dependencies.length
+ ? task.dependencies
+ .map((dep) => {
+ const depId =
+ typeof dep === "object" && dep !== null && dep.taskId
+ ? dep.taskId
+ : dep;
+ const depTask = tasks.find((t) => t.id === depId);
+ // Translate the fallback text for unknown dependency
+ const depName = depTask
+ ? depTask.name
+ : `${translate("task_detail_unknown_dependency")}(${depId})`;
+ const span = document.createElement("span");
+ span.className = "dependency-tag";
+ span.dataset.id = depId;
+ span.textContent = depName;
+ span.onclick = () => highlightNode(depId);
+ return span.outerHTML;
+ })
+ .join("")
+ : `${translate(
+ "task_detail_no_dependencies"
+ )}`; // Translate placeholder
+ detailDependencies.innerHTML = dependenciesHtml;
+ }
+
+ if (detailRelatedFiles) {
+ const relatedFilesHtml =
+ task.relatedFiles && task.relatedFiles.length
+ ? task.relatedFiles
+ .map((file) => {
+ const span = document.createElement("span");
+ span.className = "file-tag";
+ span.title = file.description || "";
+ const pathText = document.createTextNode(`${file.path} `);
+ const small = document.createElement("small");
+ small.textContent = `(${file.type})`; // Type is likely technical, maybe no translation needed?
+ span.appendChild(pathText);
+ span.appendChild(small);
+ return span.outerHTML;
+ })
+ .join("")
+ : `${translate(
+ "task_detail_no_related_files"
+ )}`; // Translate placeholder
+ detailRelatedFiles.innerHTML = relatedFilesHtml;
+ }
+
+ // --- 原來的 innerHTML 賦值已移除 ---
+
+ // 只調用高亮函數
+ highlightNode(taskId); // 只調用 highlightNode
+}
+
+// 渲染依賴關係圖 - 修改為全局視圖和 enter/update/exit 模式
+function renderDependencyGraph() {
+ if (!dependencyGraphElement || !window.d3) {
+ console.warn("D3 or dependency graph element not found.");
+ if (dependencyGraphElement) {
+ // 首次或D3丟失時顯示提示,不清空已有的圖
+ if (!dependencyGraphElement.querySelector("svg")) {
+ dependencyGraphElement.innerHTML = `${translate(
+ "error_loading_graph_d3" // Use a specific key
+ )}
`;
+ }
+ }
+ return;
+ }
+
+ // 如果沒有任務,清空圖表並顯示提示
+ if (tasks.length === 0) {
+ dependencyGraphElement.innerHTML = `${translate(
+ "dependency_graph_placeholder_empty"
+ )}
`;
+ // 重置 SVG 和 simulation 變數,以便下次正確初始化
+ svg = null;
+ g = null;
+ simulation = null;
+ return;
+ }
+
+ // 1. 準備節點 (Nodes) 和連結 (Links)
+ const nodes = tasks.map((task) => ({
+ id: task.id,
+ name: task.name,
+ status: task.status,
+ // 保留現有位置以便平滑過渡
+ x: simulation?.nodes().find((n) => n.id === task.id)?.x,
+ y: simulation?.nodes().find((n) => n.id === task.id)?.y,
+ fx: simulation?.nodes().find((n) => n.id === task.id)?.fx, // 保留固定位置
+ fy: simulation?.nodes().find((n) => n.id === task.id)?.fy,
+ }));
+
+ const links = [];
+ tasks.forEach((task) => {
+ if (task.dependencies && task.dependencies.length > 0) {
+ task.dependencies.forEach((dep) => {
+ const sourceId = typeof dep === "object" ? dep.taskId : dep;
+ const targetId = task.id;
+ if (
+ nodes.some((n) => n.id === sourceId) &&
+ nodes.some((n) => n.id === targetId)
+ ) {
+ // 確保 link 的 source/target 是 ID,以便力導向識別
+ links.push({ source: sourceId, target: targetId });
+ } else {
+ console.warn(
+ `Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.`
+ );
+ }
+ });
+ }
+ });
+
+ // 2. D3 繪圖設置與更新
+ const width = dependencyGraphElement.clientWidth;
+ const height = dependencyGraphElement.clientHeight || 400;
+
+ if (!svg) {
+ // --- 首次渲染 ---
+ console.log("First render of dependency graph");
+ dependencyGraphElement.innerHTML = ""; // 清空 placeholder
+
+ svg = d3
+ .select(dependencyGraphElement)
+ .append("svg")
+ .attr("viewBox", [0, 0, width, height])
+ .attr("preserveAspectRatio", "xMidYMid meet");
+
+ g = svg.append("g"); // 主要組,用於縮放和平移
+
+ // 添加縮放和平移
+ svg.call(
+ d3.zoom().on("zoom", (event) => {
+ g.attr("transform", event.transform);
+ })
+ );
+
+ // 添加箭頭定義
+ g.append("defs")
+ .append("marker")
+ .attr("id", "arrowhead")
+ .attr("viewBox", "-0 -5 10 10")
+ .attr("refX", 25)
+ .attr("refY", 0)
+ .attr("orient", "auto")
+ .attr("markerWidth", 8)
+ .attr("markerHeight", 8)
+ .append("path")
+ .attr("d", "M0,-5L10,0L0,5")
+ .attr("fill", "#999");
+
+ // 初始化力導向模擬
+ simulation = d3
+ .forceSimulation() // 初始化時不傳入 nodes
+ .force(
+ "link",
+ d3
+ .forceLink()
+ .id((d) => d.id)
+ .distance(100) // 指定 id 訪問器
+ )
+ .force("charge", d3.forceManyBody().strength(-300))
+ .force("center", d3.forceCenter(width / 2, height / 2))
+ .force("collide", d3.forceCollide().radius(30))
+ .on("tick", ticked); // 綁定 tick 事件處理函數
+
+ // 添加用於存放連結和節點的組
+ g.append("g").attr("class", "links");
+ g.append("g").attr("class", "nodes");
+ } else {
+ // --- 更新渲染 ---
+ console.log("Updating dependency graph");
+ // 更新 SVG 尺寸和中心力 (如果窗口大小改變)
+ svg.attr("viewBox", [0, 0, width, height]);
+ simulation.force("center", d3.forceCenter(width / 2, height / 2));
+ }
+
+ // 3. 更新連結
+ const linkSelection = g
+ .select(".links") // 選擇放置連結的 g 元素
+ .selectAll("line.link")
+ .data(
+ links,
+ (d) => `${d.source.id || d.source}-${d.target.id || d.target}`
+ ); // Key function 基於 source/target ID
+
+ // Exit - 移除舊連結
+ linkSelection
+ .exit()
+ .transition("exit")
+ .duration(300)
+ .attr("stroke-opacity", 0)
+ .remove();
+
+ // Enter - 添加新連結
+ const linkEnter = linkSelection
+ .enter()
+ .append("line")
+ .attr("class", "link")
+ .attr("stroke", "#999")
+ .attr("marker-end", "url(#arrowhead)")
+ .attr("stroke-opacity", 0); // 初始透明
+
+ // Update + Enter - 更新所有連結的屬性 (合併 enter 和 update 選擇集)
+ const linkUpdate = linkSelection.merge(linkEnter);
+
+ linkUpdate
+ .transition("update")
+ .duration(500)
+ .attr("stroke-opacity", 0.6)
+ .attr("stroke-width", 1.5);
+
+ // 4. 更新節點
+ const nodeSelection = g
+ .select(".nodes") // 選擇放置節點的 g 元素
+ .selectAll("g.node-item")
+ .data(nodes, (d) => d.id); // 使用 ID 作為 key
+
+ // Exit - 移除舊節點
+ nodeSelection
+ .exit()
+ .transition("exit")
+ .duration(300)
+ .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(0)`) // 從當前位置縮放消失
+ .attr("opacity", 0)
+ .remove();
+
+ // Enter - 添加新節點組
+ const nodeEnter = nodeSelection
+ .enter()
+ .append("g")
+ .attr("class", (d) => `node-item status-${getStatusClass(d.status)}`) // 使用輔助函數設置 class
+ .attr("data-id", (d) => d.id)
+ // 初始位置:從模擬計算的位置(如果存在)或隨機位置出現,初始縮放為0
+ .attr(
+ "transform",
+ (d) =>
+ `translate(${d.x || Math.random() * width}, ${
+ d.y || Math.random() * height
+ }) scale(0)`
+ )
+ .attr("opacity", 0)
+ .call(drag(simulation)); // 添加拖拽
+
+ // 添加圓形到 Enter 選擇集
+ nodeEnter
+ .append("circle")
+ .attr("r", 10)
+ .attr("stroke", "#fff")
+ .attr("stroke-width", 1.5);
+ // 顏色將在 merge 後通過 update 過渡設置
+
+ // 添加文字到 Enter 選擇集
+ nodeEnter
+ .append("text")
+ .attr("x", 15)
+ .attr("y", 3)
+ .text((d) => d.name)
+ .attr("font-size", "10px")
+ .attr("fill", "#ccc");
+
+ // 添加標題 (tooltip) 到 Enter 選擇集
+ nodeEnter
+ .append("title")
+ .text((d) => `${d.name} (${getStatusText(d.status)})`);
+
+ // 添加點擊事件到 Enter 選擇集
+ nodeEnter.on("click", (event, d) => {
+ selectTask(d.id);
+ event.stopPropagation();
+ });
+
+ // Update + Enter - 合併並更新所有節點
+ const nodeUpdate = nodeSelection.merge(nodeEnter);
+
+ // 過渡到最終位置和狀態
+ nodeUpdate
+ .transition("update")
+ .duration(500)
+ .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(1)`) // 移動到模擬位置並恢復大小
+ .attr("opacity", 1);
+
+ // 更新節點顏色 (單獨過渡)
+ nodeUpdate
+ .select("circle")
+ .transition("color")
+ .duration(500)
+ .attr("fill", getNodeColor); // 使用已有的 getNodeColor 函數
+
+ // 更新節點狀態 Class (即時更新,無需過渡)
+ nodeUpdate.attr(
+ "class",
+ (d) => `node-item status-${getStatusClass(d.status)}`
+ );
+
+ // << 新增:重新定義 drag 函數 >>
+ function drag(simulation) {
+ function dragstarted(event, d) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event, d) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event, d) {
+ if (!event.active) simulation.alphaTarget(0);
+ // 取消固定位置,讓節點可以繼續被力導引影響 (如果需要)
+ // d.fx = null;
+ // d.fy = null;
+ // 或者保留固定位置直到再次拖動
+ }
+
+ return d3
+ .drag()
+ .on("start", dragstarted)
+ .on("drag", dragged)
+ .on("end", dragended);
+ }
+ // << drag 函數定義結束 >>
+
+ // 5. 更新力導向模擬
+ simulation.nodes(nodes); // 在處理完 enter/exit 後更新模擬節點
+ simulation.force("link").links(links); // 更新模擬連結
+ simulation.alpha(0.3).restart(); // 重新激活模擬
+}
+
+// Tick 函數: 更新節點和連結位置
+function ticked() {
+ if (!g) return;
+
+ // 更新連結位置
+ g.select(".links")
+ .selectAll("line.link")
+ .attr("x1", (d) => d.source.x)
+ .attr("y1", (d) => d.source.y)
+ .attr("x2", (d) => d.target.x)
+ .attr("y2", (d) => d.target.y);
+
+ // 更新節點組位置
+ g.select(".nodes")
+ .selectAll("g.node-item")
+ // << 修改:添加座標後備值 >>
+ .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`);
+}
+
+// 函數:根據節點數據返回顏色 (示例)
+function getNodeColor(nodeData) {
+ switch (nodeData.status) {
+ case "已完成":
+ return "var(--secondary-color)";
+ case "進行中":
+ return "var(--primary-color)";
+ case "待處理":
+ return "#f1c40f"; // 與進度條和狀態標籤一致
+ default:
+ return "#7f8c8d"; // 未知狀態
+ }
+}
+
+// 輔助函數
+function getStatusText(status) {
+ switch (status) {
+ case "pending":
+ return translate("status_pending");
+ case "in_progress":
+ return translate("status_in_progress");
+ case "completed":
+ return translate("status_completed");
+ default:
+ return status;
+ }
+}
+
+function updateCurrentTime() {
+ const now = new Date();
+ // 保留原始格式,如果需要本地化時間,可以在此處使用 translate 或其他庫
+ const timeString = now.toLocaleString(); // 考慮是否需要基於 currentLang 格式化
+ if (currentTimeElement) {
+ // 將靜態文本和動態時間分開
+ const footerTextElement = currentTimeElement.parentNode.childNodes[0];
+ if (footerTextElement && footerTextElement.nodeType === Node.TEXT_NODE) {
+ footerTextElement.nodeValue = translate("footer_copyright");
+ }
+ currentTimeElement.textContent = timeString;
+ }
+}
+// 更新項目進度指示器
+function updateProgressIndicator() {
+ const totalTasks = tasks.length;
+ if (totalTasks === 0) {
+ progressIndicator.style.display = "none"; // 沒有任務時隱藏
+ return;
+ }
+
+ progressIndicator.style.display = "block"; // 確保顯示
+
+ const completedTasks = tasks.filter(
+ (task) => task.status === "已完成"
+ ).length;
+ const inProgressTasks = tasks.filter(
+ (task) => task.status === "進行中"
+ ).length;
+ const pendingTasks = tasks.filter((task) => task.status === "待處理").length;
+
+ const completedPercent =
+ totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
+ const inProgressPercent =
+ totalTasks > 0 ? (inProgressTasks / totalTasks) * 100 : 0;
+ const pendingPercent = totalTasks > 0 ? (pendingTasks / totalTasks) * 100 : 0;
+
+ progressCompleted.style.width = `${completedPercent}%`;
+ progressInProgress.style.width = `${inProgressPercent}%`;
+ progressPending.style.width = `${pendingPercent}%`;
+
+ // 更新標籤 (使用 translate)
+ progressLabels.innerHTML = `
+ ${translate(
+ "progress_completed"
+ )}: ${completedTasks} (${completedPercent.toFixed(1)}%)
+ ${translate(
+ "progress_in_progress"
+ )}: ${inProgressTasks} (${inProgressPercent.toFixed(1)}%)
+ ${translate(
+ "progress_pending"
+ )}: ${pendingTasks} (${pendingPercent.toFixed(1)}%)
+ ${translate(
+ "progress_total"
+ )}: ${totalTasks}
+ `;
+}
+
+// 新增:渲染全局分析結果
+function renderGlobalAnalysisResult() {
+ let targetElement = document.getElementById("global-analysis-result");
+
+ // 如果元素不存在,嘗試創建並添加到合適的位置 (例如 header 或 main content 前)
+ if (!targetElement) {
+ targetElement = document.createElement("div");
+ targetElement.id = "global-analysis-result";
+ targetElement.className = "global-analysis-section"; // 添加樣式 class
+ // 嘗試插入到 header 之後或 main 之前
+ const header = document.querySelector("header");
+ const mainContent = document.querySelector("main");
+ if (header && header.parentNode) {
+ header.parentNode.insertBefore(targetElement, header.nextSibling);
+ } else if (mainContent && mainContent.parentNode) {
+ mainContent.parentNode.insertBefore(targetElement, mainContent);
+ } else {
+ // 作為最後手段,添加到 body 開頭
+ document.body.insertBefore(targetElement, document.body.firstChild);
+ }
+ }
+
+ if (globalAnalysisResult) {
+ targetElement.innerHTML = `
+ ${translate(
+ "global_analysis_title"
+ )}
+ ${globalAnalysisResult}
+ `;
+ targetElement.style.display = "block";
+ } else {
+ targetElement.style.display = "none"; // 如果沒有結果則隱藏
+ targetElement.innerHTML = ""; // 清空內容
+ }
+}
+
+// 新增:高亮依賴圖中的節點
+function highlightNode(taskId, status = null) {
+ if (!g || !window.d3) return;
+
+ // 清除所有節點的高亮
+ g.select(".nodes") // 從 g 開始選擇
+ .selectAll("g.node-item")
+ .classed("highlighted", false);
+
+ if (!taskId) return;
+
+ // 高亮選中的節點
+ const selectedNode = g
+ .select(".nodes") // 從 g 開始選擇
+ .select(`g.node-item[data-id="${taskId}"]`);
+ if (!selectedNode.empty()) {
+ selectedNode.classed("highlighted", true);
+ // 可以選擇性地將選中節點帶到最前面
+ // selectedNode.raise();
+ }
+}
+
+// 新增:輔助函數獲取狀態 class (應放在 ticked 函數之後,getNodeColor 之前或之後均可)
+function getStatusClass(status) {
+ return status ? status.replace(/_/g, "-") : "unknown"; // 替換所有下劃線
+}
+
+// 函數:啟用節點拖拽 (保持不變)
+// ... drag ...
diff --git a/src/public/style.css b/src/public/style.css
new file mode 100644
index 0000000..f019803
--- /dev/null
+++ b/src/public/style.css
@@ -0,0 +1,461 @@
+:root {
+ --primary-color: #3498db;
+ --secondary-color: #2ecc71;
+ --background-color: #1a1a2e;
+ --panel-color: #16213e;
+ --text-color: #f0f0f0;
+ --accent-color: #4cd137;
+ --danger-color: #e74c3c;
+ --border-radius: 8px;
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ line-height: 1.6;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: 20px;
+}
+
+h1 {
+ font-size: 1.8rem;
+ font-weight: 600;
+ color: var(--primary-color);
+}
+
+.status-bar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.status-indicator {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background-color: var(--accent-color);
+ box-shadow: 0 0 8px var(--accent-color);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+main {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-template-areas:
+ "dependency"
+ "bottom";
+ gap: 20px;
+ flex-grow: 1;
+}
+
+.task-panel,
+.task-details {
+ background-color: var(--panel-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.dependency-view {
+ grid-area: dependency;
+ background-color: var(--panel-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.bottom-panels {
+ grid-area: bottom;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ overflow: hidden;
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+h2 {
+ font-size: 1.2rem;
+ font-weight: 500;
+}
+
+select {
+ background-color: rgba(0, 0, 0, 0.3);
+ color: var(--text-color);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 5px 10px;
+ border-radius: 4px;
+ outline: none;
+}
+
+.task-list,
+.dependency-graph,
+#task-details-content {
+ padding: 15px;
+ flex-grow: 1;
+ overflow-y: auto;
+}
+
+.dependency-graph {
+ min-height: 300px;
+}
+
+.task-item {
+ padding: 15px;
+ margin-bottom: 15px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: var(--border-radius);
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ position: relative;
+ overflow: hidden;
+}
+
+.task-item::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 4px;
+ background-color: transparent;
+ transition: background-color 0.3s;
+}
+
+.task-item.status-pending::before {
+ background-color: #f1c40f;
+}
+
+.task-item.status-in-progress::before {
+ background-color: var(--primary-color);
+}
+
+.task-item.status-completed::before {
+ background-color: var(--secondary-color);
+}
+
+.task-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+}
+
+.task-item.selected {
+ background-color: rgba(52, 152, 219, 0.15);
+ box-shadow: 0 0 0 2px var(--primary-color);
+}
+
+.task-item h3 {
+ font-size: 1.1rem;
+ margin-bottom: 5px;
+}
+
+.task-meta {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.85rem;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.task-status {
+ padding: 2px 8px;
+ border-radius: 20px;
+ font-size: 0.7rem;
+ font-weight: 500;
+}
+
+.status-pending {
+ background-color: rgba(241, 196, 15, 0.2);
+ color: #f1c40f;
+}
+
+.status-in-progress {
+ background-color: rgba(52, 152, 219, 0.2);
+ color: #3498db;
+}
+
+.status-completed {
+ background-color: rgba(46, 204, 113, 0.2);
+ color: #2ecc71;
+}
+
+.placeholder {
+ text-align: center;
+ color: rgba(255, 255, 255, 0.5);
+ padding: 50px 0;
+}
+
+.loading {
+ text-align: center;
+ padding: 20px;
+ color: rgba(255, 255, 255, 0.7);
+ animation: fadeInOut 1.5s infinite;
+}
+
+@keyframes fadeInOut {
+ 0%,
+ 100% {
+ opacity: 0.5;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+.task-details-header {
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.task-details-header h3 {
+ font-size: 1.3rem;
+ margin-bottom: 5px;
+}
+
+.task-details-section {
+ margin-bottom: 20px;
+}
+
+.task-details-section h4 {
+ font-size: 1rem;
+ color: var(--primary-color);
+ margin-bottom: 10px;
+}
+
+.dependencies,
+.related-files {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.dependency-tag,
+.file-tag {
+ background-color: rgba(0, 0, 0, 0.3);
+ border-radius: 4px;
+ padding: 5px 10px;
+ font-size: 0.8rem;
+}
+
+pre {
+ background-color: rgba(0, 0, 0, 0.3);
+ padding: 15px;
+ border-radius: var(--border-radius);
+ overflow-x: auto;
+ margin: 10px 0;
+ font-family: "Consolas", "Monaco", monospace;
+ font-size: 0.9rem;
+}
+
+footer {
+ margin-top: 20px;
+ text-align: center;
+ font-size: 0.8rem;
+ color: rgba(255, 255, 255, 0.5);
+ padding: 10px 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* 進度指示器樣式 */
+.progress-indicator {
+ margin-bottom: 20px;
+ padding: 15px;
+ background-color: var(--panel-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+}
+
+.progress-bar-container {
+ display: flex;
+ height: 10px;
+ border-radius: 5px;
+ overflow: hidden;
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+.progress-segment {
+ height: 100%;
+ transition: width 0.5s ease-in-out;
+}
+
+.progress-completed {
+ background-color: var(--secondary-color);
+}
+
+.progress-in-progress {
+ background-color: var(--primary-color);
+}
+
+.progress-pending {
+ background-color: #f1c40f; /* 與status-pending一致 */
+}
+
+.progress-labels {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 10px;
+ font-size: 0.8rem;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.progress-labels span {
+ padding: 2px 5px;
+}
+
+.label-completed {
+ color: var(--secondary-color);
+}
+
+.label-in-progress {
+ color: var(--primary-color);
+}
+
+.label-pending {
+ color: #f1c40f;
+}
+
+/* 依賴關係圖樣式 */
+.dependency-graph svg {
+ display: block; /* 避免底部多餘空間 */
+ width: 100%;
+ height: 100%;
+}
+
+.nodes g.node-item circle {
+ transition: r 0.2s, stroke 0.2s;
+ /* 根據類型設置基礎顏色 */
+}
+.nodes g.type-current circle {
+ fill: var(--primary-color); /* 當前選中任務 */
+}
+.nodes g.type-dependency circle {
+ fill: var(--danger-color); /* 前置依賴任務 */
+}
+.nodes g.type-dependent circle {
+ fill: var(--secondary-color); /* 後置依賴任務 */
+}
+.nodes g.type-unknown circle {
+ fill: #7f8c8d; /* 未知任務 */
+}
+
+/* 根據狀態調整節點透明度或邊框 */
+.nodes g.status-completed circle {
+ opacity: 0.6;
+}
+.nodes g.status-in-progress circle {
+ /* 可以添加特殊效果,如描邊動畫 */
+ stroke: var(--accent-color);
+ stroke-width: 2px;
+}
+
+.nodes g.node-item:hover circle {
+ r: 14; /* 懸停時放大 */
+ stroke: #fff;
+ stroke-width: 2.5px;
+}
+
+.nodes g.node-item text {
+ fill: var(--text-color);
+ font-size: 10px;
+ pointer-events: none; /* 避免文本干擾點擊 */
+}
+
+/* 新增:高亮節點樣式 */
+g.node-item.highlighted circle {
+ stroke: var(--accent-color) !important; /* 使用重要標誌確保覆蓋 */
+ stroke-width: 3px !important;
+}
+
+@media (max-width: 768px) {
+ main {
+ grid-template-rows: auto auto; /* Stack dependency and bottom panels */
+ grid-template-areas:
+ "dependency"
+ "bottom";
+ }
+
+ .bottom-panels {
+ grid-template-columns: 1fr; /* Stack task list and details */
+ grid-template-rows: auto auto; /* Or let them take natural height */
+ }
+
+ .task-panel,
+ .dependency-view,
+ .task-details {
+ /* grid-column: 1 / -1; No longer needed */
+ min-height: 300px; /* Ensure panels have some height */
+ }
+}
+
+/* 過濾器區域樣式 */
+.filters {
+ display: flex;
+ gap: 10px;
+}
+
+.filters input[type="text"],
+.filters select {
+ background-color: rgba(0, 0, 0, 0.3);
+ color: var(--text-color);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 5px 10px;
+ border-radius: 4px;
+ outline: none;
+ font-size: 0.9rem;
+}
+
+.filters input[type="text"] {
+ flex-grow: 1; /* 讓搜索框佔據更多空間 */
+ min-width: 150px;
+}