Skip to main content
Glama

mcp-chain-of-thought

script.js42.7 kB
// Global variables let tasks = []; let selectedTaskId = null; let searchTerm = ""; let sortOption = "date-asc"; let globalAnalysisResult = null; // Added: Store global analysis result let svg, g, simulation; // Modified: Define D3 related variables let conversationHistory = []; // Added: Store conversation history for selected task // Added: i18n global variables let currentLang = "en"; // Default language let translations = {}; // Store loaded translations // DOM elements 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" ); // Assuming this element exists in HTML const langSwitcher = document.getElementById("lang-switcher"); // Added: Get the language switcher element // Initialization document.addEventListener("DOMContentLoaded", () => { // fetchTasks(); // Will be triggered by initI18n() initI18n(); // Added: Initialize i18n updateCurrentTime(); setInterval(updateCurrentTime, 1000); // Event listeners // statusFilter.addEventListener("change", renderTasks); // Will be triggered by changeLanguage or after applyTranslations if (statusFilter) { statusFilter.addEventListener("change", renderTasks); } // Added: Search and sort event listeners 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(); }); } // Added: Set up SSE connection setupSSE(); // Added: Language switcher event listener if (langSwitcher) { langSwitcher.addEventListener("change", (e) => changeLanguage(e.target.value) ); } }); // Added: i18n core functions // 1. Language detection (localStorage > navigator.language > 'en') function detectLanguage() { const savedLang = localStorage.getItem("lang"); if (savedLang && ["en", "zh-TW"].includes(savedLang)) { // Ensure saved language is valid return savedLang; } // Check browser language 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"; // Simplified Chinese also falls back to Traditional for now if (browserLang.toLowerCase().startsWith("en")) return "en"; } return "en"; // Default } // 2. Asynchronously load translation files 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. Translation function function translate(key, replacements = {}) { let translated = translations[key] || key; // Fallback to key itself // Simple placeholder replacement (e.g., {message}) for (const placeholder in replacements) { translated = translated.replace( `{${placeholder}}`, replacements[placeholder] ); } return translated; } // 4. Apply translations to DOM (process 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); // Prioritize specific attributes if (el.hasAttribute("placeholder")) { el.placeholder = translatedText; } else if (el.hasAttribute("title")) { el.title = translatedText; } else if (el.tagName === "OPTION") { el.textContent = translatedText; // If needed, also translate value, but usually not } else { // For most elements, set textContent el.textContent = translatedText; } }); // Manually update elements without data-key (if any) // For example, if footer time format needs localization, can handle here // updateCurrentTime(); // Ensure time format might also update (if needed) } // 5. Initialize i18n async function initI18n() { currentLang = detectLanguage(); console.log(`Initializing i18n with language: ${currentLang}`); localStorage.setItem("lang", currentLang); // Ensure lang is saved // Added: Set initial value for the switcher if (langSwitcher) { langSwitcher.value = currentLang; } await loadTranslations(currentLang); applyTranslations(); await fetchTasks(); } // Added: Language switcher function 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..."); // Re-render components that need translation renderTasks(); if (selectedTaskId) { const task = tasks.find((t) => t.id === selectedTaskId); if (task) { selectTask(selectedTaskId); // Ensure passing ID, let selectTask re-find and render } else { // If selected task no longer exists, clear details taskDetailsContent.innerHTML = `<p class="placeholder">${translate( "task_details_placeholder" )}</p>`; selectedTaskId = null; highlightNode(null); } } else { // If no task is selected, ensure details panel shows placeholder taskDetailsContent.innerHTML = `<p class="placeholder">${translate( "task_details_placeholder" )}</p>`; } renderDependencyGraph(); // Re-render graph (might contain placeholder) updateProgressIndicator(); // Re-render progress bar (includes labels) renderGlobalAnalysisResult(); // Re-render global analysis (title) // Ensure dropdown value is consistent with current language if (langSwitcher) langSwitcher.value = currentLang; console.log("Language change complete."); }) .catch((error) => { console.error("Error changing language:", error); // Can add user feedback, e.g. show error message showTemporaryError("Failed to change language. Please try again."); // Need translation key }); } // --- i18n core functions end --- // Get task data async function fetchTasks() { try { // Show loading (now using translation) if (tasks.length === 0) { taskListElement.innerHTML = `<div class="loading">${translate( "task_list_loading" )}</div>`; } 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 || []; // Extract global analysis result (find first non-empty) let foundAnalysisResult = null; for (const task of newTasks) { if (task.analysisResult) { foundAnalysisResult = task.analysisResult; break; // Found one is enough } } // Only update if found result is different from current stored if (foundAnalysisResult !== globalAnalysisResult) { globalAnalysisResult = foundAnalysisResult; renderGlobalAnalysisResult(); // Update display } // --- Smart update logic (initial - still needs improvement to avoid flickering) --- // Simply compare task count or identifier to decide whether to re-render // Ideal situation should compare content of each task and perform DOM update const tasksChanged = didTasksChange(tasks, newTasks); if (tasksChanged) { tasks = newTasks; // Update global task list console.log("Tasks updated via fetch, re-rendering..."); renderTasks(); updateProgressIndicator(); renderDependencyGraph(); // Update graph } else { console.log( "No significant task changes detected, skipping full re-render." ); // If no need to re-render list, possibly just update progress bar updateProgressIndicator(); // Consider whether to update graph (if status might change) // renderDependencyGraph(); // Temporarily commented out unless status change is critical } // *** Remove setTimeout polling *** // setTimeout(fetchTasks, 30000); } catch (error) { console.error("Error fetching tasks:", error); // Avoid overwriting existing list unless initial load fails if (tasks.length === 0) { taskListElement.innerHTML = `<div class="error">${translate( "error_loading_tasks", { message: error.message } )}</div>`; if (progressIndicator) progressIndicator.style.display = "none"; if (dependencyGraphElement) dependencyGraphElement.innerHTML = `<div class="error">${translate( "error_loading_graph" )}</div>`; } else { showTemporaryError( translate("error_updating_tasks", { message: error.message }) ); } } } // Added: Set up Server-Sent Events connection 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); // Can do more complex judgment based on event.data content, currently just update if receive message }; evtSource.addEventListener("update", function (event) { console.log("SSE 'update' event received:", event.data); // Receive update event, re-fetch task list fetchTasks(); }); evtSource.onerror = function (err) { console.error("EventSource failed:", err); // Can implement re-connect logic evtSource.close(); // Close error connection // Delay a bit later to try re-connect setTimeout(setupSSE, 5000); // 5 seconds later retry }; evtSource.onopen = function () { console.log("SSE connection opened."); }; } // Added: Helper function to compare task lists for changes (most comprehensive version) 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; } // Added: Function to show temporary error message function showTemporaryError(message) { const errorElement = document.createElement("div"); errorElement.className = "temporary-error"; errorElement.textContent = message; // Keep message itself document.body.appendChild(errorElement); setTimeout(() => { errorElement.remove(); }, 3000); // Show for 3 seconds } // Render task list - *** Needs further optimization to implement smart update *** function renderTasks() { console.log("Rendering tasks..."); // Add log 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); } }); // --- Simple brute force replacement (will cause flickering) --- // TODO: Implement DOM Diffing or more intelligent update strategy if (filteredTasks.length === 0) { taskListElement.innerHTML = `<div class="placeholder">${translate( "task_list_empty" )}</div>`; } else { taskListElement.innerHTML = filteredTasks .map( (task) => ` <div class="task-item status-${task.status.replace( "_", "-" )}" data-id="${task.id}" onclick="selectTask('${task.id}')"> <h3>${task.name}</h3> <div class="task-meta"> <span class="task-status status-${task.status.replace( "_", "-" )}">${getStatusText(task.status)}</span> </div> </div> ` ) .join(""); } // --- End simple brute force replacement --- // Re-apply selected state 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 { // If selected task no longer exists in new list, clear selection console.log( `Selected task ${selectedTaskId} no longer exists, clearing selection.` ); selectedTaskId = null; taskDetailsContent.innerHTML = `<p class="placeholder">${translate( "task_details_placeholder" )}</p>`; highlightNode(null); // Clear graph highlight } } } // Select task async function selectTask(taskId) { // Clear old selected state and highlight if (selectedTaskId) { const previousElement = document.querySelector( `.task-item[data-id="${selectedTaskId}"]` ); if (previousElement) { previousElement.classList.remove("selected"); } } // If clicked again on the same task, then clear selection if (selectedTaskId === taskId) { selectedTaskId = null; taskDetailsContent.innerHTML = `<p class="placeholder">${translate( "task_details_placeholder" )}</p>`; highlightNode(null); // Clear highlight return; } selectedTaskId = taskId; // Add new selected state const selectedElement = document.querySelector( `.task-item[data-id="${taskId}"]` ); if (selectedElement) { selectedElement.classList.add("selected"); } // Get and display task details const task = tasks.find((t) => t.id === taskId); if (!task) { taskDetailsContent.innerHTML = `<div class="placeholder">${translate( "error_task_not_found" )}</div>`; return; } // Fetch conversation history if detailed mode is enabled conversationHistory = await fetchConversationHistory(taskId); // --- Safely fill task details --- // 1. Create basic skeleton (use innerHTML, but replace dynamic content with ID'd empty elements) taskDetailsContent.innerHTML = ` <div class="task-details-header"> <h3 id="detail-name"></h3> <div class="task-meta"> <span>Status: <span id="detail-status" class="task-status"></span></span> </div> </div> <!-- Added: Condition display Summary --> <div class="task-details-section" id="detail-summary-section" style="display: none;"> <h4>Completion Summary</h4> <p id="detail-summary"></p> </div> <div class="task-details-section"> <h4>Task Description</h4> <p id="detail-description"></p> </div> <div class="task-details-section"> <h4>Implementation Guide</h4> <pre id="detail-implementation-guide"></pre> </div> <div class="task-details-section"> <h4>Verification Criteria</h4> <p id="detail-verification-criteria"></p> </div> <div class="task-details-section"> <h4>Dependencies (Prerequisites)</h4> <div class="dependencies" id="detail-dependencies"> <!-- Dependencies will be populated by JS --> </div> </div> <div class="task-details-section"> <h4>Related Files</h4> <div class="related-files" id="detail-related-files"> <!-- Related files will be populated by JS --> </div> </div> <div class="task-details-section"> <h4>Notes</h4> <p id="detail-notes"></p> </div> <!-- Conversation history section will be added here if available --> <div id="conversation-history-container"></div> `; // 2. Get corresponding elements and use textContent safely to fill content 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" ); // Added: Get Summary related elements 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"); const conversationHistoryContainer = document.getElementById("conversation-history-container"); 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"); // Added: Fill Summary (if exists and completed) if (task.summary && detailSummarySection && detailSummary) { detailSummary.textContent = task.summary; detailSummarySection.style.display = "block"; // Show block } else if (detailSummarySection) { detailSummarySection.style.display = "none"; // Hide block } if (detailNotes) detailNotes.textContent = task.notes || translate("task_detail_no_notes"); // 3. Dynamically generate dependencies and related files (these can include safe HTML structures like 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("") : `<span class="placeholder">${translate( "task_detail_no_dependencies" )}</span>`; // 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("") : `<span class="placeholder">${translate( "task_detail_no_related_files" )}</span>`; // Translate placeholder detailRelatedFiles.innerHTML = relatedFilesHtml; } // Add conversation history section if available if (conversationHistoryContainer && conversationHistory && conversationHistory.length > 0) { conversationHistoryContainer.innerHTML = renderConversationHistory(conversationHistory); // Add event listeners for toggle buttons document.querySelectorAll(".toggle-btn").forEach((btn) => { const targetId = btn.getAttribute("data-target"); const targetElement = document.getElementById(targetId); btn.addEventListener("click", () => { targetElement.classList.toggle("collapsed"); btn.classList.toggle("collapsed"); }); }); } // Just call highlight function highlightNode(taskId); } // Render dependency relationship graph - Modified to global view and enter/update/exit mode function renderDependencyGraph() { if (!dependencyGraphElement || !window.d3) { console.warn("D3 or dependency graph element not found."); if (dependencyGraphElement) { // First or D3 loss time display prompt, don't clear existing graph if (!dependencyGraphElement.querySelector("svg")) { dependencyGraphElement.innerHTML = `<p class="placeholder">${translate( "error_loading_graph_d3" // Use a specific key )}</p>`; } } return; } // If no tasks, clear graph and display prompt if (tasks.length === 0) { dependencyGraphElement.innerHTML = `<p class="placeholder">${translate( "dependency_graph_placeholder_empty" )}</p>`; // Reset SVG and simulation variables, so next time correctly initialize svg = null; g = null; simulation = null; return; } // 1. Prepare nodes (Nodes) and links (Links) const nodes = tasks.map((task) => ({ id: task.id, name: task.name, status: task.status, // Keep existing position for smooth transition 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, // Keep fixed position 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) ) { // Ensure link's source/target is ID, for force directed recognition links.push({ source: sourceId, target: targetId }); } else { console.warn( `Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.` ); } }); } }); // 2. D3 drawing setup and update const width = dependencyGraphElement.clientWidth; const height = dependencyGraphElement.clientHeight || 400; if (!svg) { // --- First render --- console.log("First render of dependency graph"); dependencyGraphElement.innerHTML = ""; // Clear placeholder svg = d3 .select(dependencyGraphElement) .append("svg") .attr("viewBox", [0, 0, width, height]) .attr("preserveAspectRatio", "xMidYMid meet"); g = svg.append("g"); // Main group, for zooming and panning // Add zooming and panning svg.call( d3.zoom().on("zoom", (event) => { g.attr("transform", event.transform); }) ); // Add arrow definition 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"); // Initialize force directed simulation simulation = d3 .forceSimulation() // Initialize without nodes .force( "link", d3 .forceLink() .id((d) => d.id) .distance(100) // Specify id accessor ) .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(30)) .on("tick", ticked); // Bind tick event processing function // Add for storing links and nodes g.append("g").attr("class", "links"); g.append("g").attr("class", "nodes"); } else { // --- Update render --- console.log("Updating dependency graph"); // Update SVG size and center force (if window size changes) svg.attr("viewBox", [0, 0, width, height]); simulation.force("center", d3.forceCenter(width / 2, height / 2)); } // 3. Update links const linkSelection = g .select(".links") // Select g element for placing links .selectAll("line.link") .data( links, (d) => `${d.source.id || d.source}-${d.target.id || d.target}` ); // Key function based on source/target ID // Exit - Remove old links linkSelection .exit() .transition("exit") .duration(300) .attr("stroke-opacity", 0) .remove(); // Enter - Add new links const linkEnter = linkSelection .enter() .append("line") .attr("class", "link") .attr("stroke", "#999") .attr("marker-end", "url(#arrowhead)") .attr("stroke-opacity", 0); // Initial transparent // Update + Enter - Update all link attributes (Merge enter and update selection) const linkUpdate = linkSelection.merge(linkEnter); linkUpdate .transition("update") .duration(500) .attr("stroke-opacity", 0.6) .attr("stroke-width", 1.5); // 4. Update nodes const nodeSelection = g .select(".nodes") // Select g element for placing nodes .selectAll("g.node-item") .data(nodes, (d) => d.id); // Use ID as key // Exit - Remove old nodes nodeSelection .exit() .transition("exit") .duration(300) .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(0)`) // Scale out from current position .attr("opacity", 0) .remove(); // Enter - Add new node group const nodeEnter = nodeSelection .enter() .append("g") .attr("class", (d) => `node-item status-${getStatusClass(d.status)}`) // Use helper function to set class .attr("data-id", (d) => d.id) // Initial position: From simulated calculation position (if exists) or random position, initial scale 0 .attr( "transform", (d) => `translate(${d.x || Math.random() * width}, ${ d.y || Math.random() * height }) scale(0)` ) .attr("opacity", 0) .call(drag(simulation)); // Add drag // Add circle to Enter selection nodeEnter .append("circle") .attr("r", 10) .attr("stroke", "#fff") .attr("stroke-width", 1.5); // Color will be set through merge and update transition // Add text to Enter selection nodeEnter .append("text") .attr("x", 15) .attr("y", 3) .text((d) => d.name) .attr("font-size", "10px") .attr("fill", "#ccc"); // Add title (tooltip) to Enter selection nodeEnter .append("title") .text((d) => `${d.name} (${getStatusText(d.status)})`); // Add click event to Enter selection nodeEnter.on("click", (event, d) => { selectTask(d.id); event.stopPropagation(); }); // Update + Enter - Merge and update all nodes const nodeUpdate = nodeSelection.merge(nodeEnter); // Transition to final position and state nodeUpdate .transition("update") .duration(500) .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(1)`) // Move to simulated position and restore size .attr("opacity", 1); // Update node color (separate transition) nodeUpdate .select("circle") .transition("color") .duration(500) .attr("fill", getNodeColor); // Use existing getNodeColor function // Update node status Class (immediate update, no transition) nodeUpdate.attr( "class", (d) => `node-item status-${getStatusClass(d.status)}` ); // << Added: Redefine drag function >> 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); // Cancel fixed position, let node continue affected by force directed influence (if needed) // d.fx = null; // d.fy = null; // Or keep fixed position until next drag } return d3 .drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); } // << drag function definition end >> // 5. Update force directed simulation simulation.nodes(nodes); // Update simulated nodes after enter/exit processing simulation.force("link").links(links); // Update simulated links simulation.alpha(0.3).restart(); // Reactivate simulation } // Tick function: Update node and link positions function ticked() { if (!g) return; // Update link positions 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); // Update node group positions g.select(".nodes") .selectAll("g.node-item") // << Modified: Add coordinate fallback value >> .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`); } // Function: Return color based on node data (example) function getNodeColor(nodeData) { switch (nodeData.status) { case "Completed": return "var(--secondary-color)"; case "In Progress": return "var(--primary-color)"; case "Pending": return "#f1c40f"; // Consistent with progress bar and status label default: return "#7f8c8d"; // Unknown status } } // Helper function 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(); // Keep original format, if time localization needed, can use translate or other library here const timeString = now.toLocaleString(); // Consider whether to base on currentLang format if (currentTimeElement) { // Separate static text and dynamic time const footerTextElement = currentTimeElement.parentNode.childNodes[0]; if (footerTextElement && footerTextElement.nodeType === Node.TEXT_NODE) { footerTextElement.nodeValue = translate("footer_copyright"); } currentTimeElement.textContent = timeString; } } // Update project progress indicator function updateProgressIndicator() { const totalTasks = tasks.length; if (totalTasks === 0) { progressIndicator.style.display = "none"; // Hide if no tasks return; } progressIndicator.style.display = "block"; // Ensure display const completedTasks = tasks.filter( (task) => task.status === "Completed" ).length; const inProgressTasks = tasks.filter( (task) => task.status === "In Progress" ).length; const pendingTasks = tasks.filter((task) => task.status === "Pending").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}%`; // Update labels (using translate) progressLabels.innerHTML = ` <span class="label-completed">${translate( "progress_completed" )}: ${completedTasks} (${completedPercent.toFixed(1)}%)</span> <span class="label-in-progress">${translate( "progress_in_progress" )}: ${inProgressTasks} (${inProgressPercent.toFixed(1)}%)</span> <span class="label-pending">${translate( "progress_pending" )}: ${pendingTasks} (${pendingPercent.toFixed(1)}%)</span> <span class="label-total">${translate( "progress_total" )}: ${totalTasks}</span> `; } // Added: Render global analysis result function renderGlobalAnalysisResult() { let targetElement = document.getElementById("global-analysis-result"); // If element does not exist, try to create and add to suitable location (e.g. header or main content before) if (!targetElement) { targetElement = document.createElement("div"); targetElement.id = "global-analysis-result"; targetElement.className = "global-analysis-section"; // Add style class // Try to insert before header or main before 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 { // As last resort, add to body start document.body.insertBefore(targetElement, document.body.firstChild); } } if (globalAnalysisResult) { targetElement.innerHTML = ` <h4 data-i18n-key="global_analysis_title">${translate( "global_analysis_title" )}</h4> <pre>${globalAnalysisResult}</pre> `; targetElement.style.display = "block"; } else { targetElement.style.display = "none"; // If no result, hide targetElement.innerHTML = ""; // Clear content } } // Added: Highlight dependency graph node function highlightNode(taskId, status = null) { if (!g || !window.d3) return; // Clear all node highlights g.select(".nodes") // Start from g selection .selectAll("g.node-item") .classed("highlighted", false); if (!taskId) return; // Highlight selected node const selectedNode = g .select(".nodes") // Start from g selection .select(`g.node-item[data-id="${taskId}"]`); if (!selectedNode.empty()) { selectedNode.classed("highlighted", true); // Can optionally bring selected node to front // selectedNode.raise(); } } // Added: Helper function to get status class (should be placed after ticked function, before getNodeColor or after) function getStatusClass(status) { return status ? status.replace(/_/g, "-") : "unknown"; // Replace all underscores } // Function: Enable node drag (keep unchanged) // ... drag ... // Function to fetch conversation history for a task async function fetchConversationHistory(taskId) { try { const response = await fetch(`/api/tasks/${taskId}/conversation`); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); return data.conversationHistory || []; } catch (error) { console.error("Error fetching conversation history:", error); return []; } } // Function to render conversation history in the task details function renderConversationHistory(history) { if (!history || history.length === 0) { return ''; } let historyHtml = ` <div class="task-details-section conversation-history-section"> <div class="section-header"> <h4 data-i18n-key="conversation_history_title">Conversation History</h4> <button class="toggle-btn" data-target="conversation-content">${translate('toggle_conversation')}</button> </div> <div id="conversation-content" class="section-content conversation-content"> `; // Sort messages by timestamp const sortedHistory = [...history].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp) ); sortedHistory.forEach(msg => { const timestamp = new Date(msg.timestamp).toLocaleString(); const role = msg.role === 'user' ? translate('user_role') : translate('assistant_role'); const toolInfo = msg.toolName ? `<span class="tool-name">${msg.toolName}</span>` : ''; historyHtml += ` <div class="message ${msg.role}-message"> <div class="message-header"> <span class="message-role">${role}</span> <span class="message-time">${timestamp}</span> ${toolInfo} </div> <div class="message-content">${msg.content}</div> </div> `; }); historyHtml += ` </div> </div> `; return historyHtml; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/liorfranko/mcp-chain-of-thought'

If you have feedback or need assistance with the MCP directory API, please join our Discord server