| |
| const mockRuns = [ |
| { |
| runId: "cfe1e00a-1823-4fd7-bca7-3f99eccf3620", |
| started_at: "2023-05-15T10:30:00Z", |
| ended_at: "2023-05-15T10:35:23Z", |
| status: "Completed", |
| completed_nodes: 9, |
| total_nodes: 9 |
| }, |
| { |
| runId: "d2e4f6a8-b1c3-4d5e-7f8g-9h0i1j2k3l4m", |
| started_at: "2023-05-15T11:15:00Z", |
| ended_at: null, |
| status: "Running", |
| completed_nodes: 4, |
| total_nodes: 9 |
| }, |
| { |
| runId: "a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6", |
| started_at: "2023-05-14T09:45:00Z", |
| ended_at: "2023-05-14T09:47:12Z", |
| status: "Failed", |
| completed_nodes: 2, |
| total_nodes: 9 |
| } |
| ]; |
|
|
| const mockRunDetails = { |
| "cfe1e00a-1823-4fd7-bca7-3f99eccf3620": { |
| runId: "cfe1e00a-1823-4fd7-bca7-3f99eccf3620", |
| status: "Completed", |
| nodes: [ |
| { |
| node_name: "search/kb", |
| status: "success", |
| input_json: { query: "VPN not connecting" }, |
| output_json: { results: ["KB001", "KB042", "KB123"] }, |
| timestamp: "2023-05-15T10:30:15Z" |
| }, |
| { |
| node_name: "validate/kb", |
| status: "success", |
| input_json: { kb_ids: ["KB001", "KB042", "KB123"] }, |
| output_json: { valid_ids: ["KB001", "KB123"] }, |
| timestamp: "2023-05-15T10:31:02Z" |
| }, |
| |
| ] |
| } |
| }; |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| |
| renderRunsTable(); |
| |
| |
| document.getElementById('newRunBtn').addEventListener('click', startNewRun); |
| document.getElementById('closeModal').addEventListener('click', () => { |
| document.getElementById('runModal').classList.add('hidden'); |
| }); |
| }); |
|
|
| function renderRunsTable() { |
| const tableBody = document.getElementById('runsTable'); |
| tableBody.innerHTML = ''; |
| |
| mockRuns.forEach(run => { |
| const row = document.createElement('tr'); |
| |
| |
| let statusBadge; |
| switch(run.status) { |
| case 'Completed': |
| statusBadge = `<span class="px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">Completed</span>`; |
| break; |
| case 'Running': |
| statusBadge = `<span class="px-2 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 running-pulse">Running</span>`; |
| break; |
| case 'Failed': |
| statusBadge = `<span class="px-2 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-800">Failed</span>`; |
| break; |
| default: |
| statusBadge = `<span class="px-2 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-800">Pending</span>`; |
| } |
| |
| |
| const progressPercent = run.completed_nodes / run.total_nodes * 100; |
| const progressBar = ` |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div class="bg-blue-600 h-2.5 rounded-full" style="width: ${progressPercent}%"></div> |
| </div> |
| <div class="text-xs text-gray-500 mt-1">${run.completed_nodes}/${run.total_nodes} nodes</div> |
| `; |
| |
| |
| const shortRunId = run.runId.substring(0, 8) + '...'; |
| |
| row.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${shortRunId}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${new Date(run.started_at).toLocaleString()}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${statusBadge}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${progressBar}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
| <button class="text-blue-600 hover:text-blue-800 view-run" data-runid="${run.runId}"> |
| <i data-feather="eye" class="w-4 h-4"></i> View |
| </button> |
| </td> |
| `; |
| |
| tableBody.appendChild(row); |
| }); |
| |
| |
| document.querySelectorAll('.view-run').forEach(button => { |
| button.addEventListener('click', (e) => { |
| const runId = e.target.closest('button').getAttribute('data-runid'); |
| showRunDetails(runId); |
| }); |
| }); |
| |
| feather.replace(); |
| }); |
|
|
| function showRunDetails(runId) { |
| const modal = document.getElementById('runModal'); |
| const detailsContainer = document.getElementById('runDetails'); |
| |
| |
| const runDetails = mockRunDetails[runId] || { |
| runId, |
| status: "Running", |
| nodes: [] |
| }; |
| |
| |
| detailsContainer.innerHTML = ` |
| <div class="mb-6"> |
| <h3 class="text-lg font-semibold mb-2">Run ID: ${runDetails.runId}</h3> |
| <div class="flex items-center"> |
| <span class="mr-2">Status:</span> |
| ${runDetails.status === 'Completed' ? |
| '<span class="px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">Completed</span>' : |
| runDetails.status === 'Running' ? |
| '<span class="px-2 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 running-pulse">Running</span>' : |
| '<span class="px-2 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-800">Failed</span>'} |
| </div> |
| </div> |
| |
| <div class="mb-8"> |
| <h3 class="text-lg font-semibold mb-4">Execution Flow</h3> |
| <div class="flex items-center justify-center overflow-x-auto py-4"> |
| ${renderFlowDiagram(runDetails.nodes)} |
| </div> |
| </div> |
| |
| <div> |
| <h3 class="text-lg font-semibold mb-4">Node Details</h3> |
| <div class="space-y-4"> |
| ${runDetails.nodes.map(node => renderNodeDetails(node)).join('')} |
| </div> |
| </div> |
| `; |
| |
| modal.classList.remove('hidden'); |
| feather.replace(); |
| } |
|
|
| function renderFlowDiagram(nodes) { |
| const allNodes = [ |
| "search/kb", "validate/kb", "search/web", "steps/perform", |
| "script/generate", "email/send", "ticket/route", "ticket/create", "issue/auto-resolve" |
| ]; |
| |
| let html = ''; |
| |
| for (let i = 0; i < allNodes.length; i++) { |
| const nodeName = allNodes[i]; |
| const nodeData = nodes.find(n => n.node_name === nodeName); |
| |
| |
| let nodeStatus; |
| if (nodeData) { |
| nodeStatus = nodeData.status === 'success' ? 'success' : 'failed'; |
| } else { |
| nodeStatus = 'pending'; |
| } |
| |
| |
| html += ` |
| <div class="node-container"> |
| <div class="node ${nodeStatus === 'success' ? 'node-success' : |
| nodeStatus === 'failed' ? 'node-failed' : |
| nodeStatus === 'running' ? 'node-running running-pulse' : 'node-pending'}" |
| title="${nodeName}"> |
| ${i+1} |
| </div> |
| ${i < allNodes.length - 1 ? ` |
| <div class="flow-line ${nodeStatus === 'success' ? 'completed' : |
| nodeStatus === 'failed' ? 'failed' : |
| nodeStatus === 'running' ? 'active' : ''}"></div> |
| ` : ''} |
| </div> |
| `; |
| } |
| |
| return html; |
| } |
|
|
| function renderNodeDetails(node) { |
| return ` |
| <div class="bg-gray-50 rounded-lg p-4"> |
| <div class="flex justify-between items-center mb-2"> |
| <h4 class="font-medium">${node.node_name}</h4> |
| <span class="px-2 py-1 rounded-full text-xs font-semibold ${ |
| node.status === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' |
| }"> |
| ${node.status} |
| </span> |
| </div> |
| <div class="text-sm text-gray-500 mb-2">${new Date(node.timestamp).toLocaleString()}</div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3"> |
| <div> |
| <h5 class="text-sm font-medium mb-1">Input</h5> |
| <div class="json-viewer">${JSON.stringify(node.input_json, null, 2)}</div> |
| </div> |
| <div> |
| <h5 class="text-sm font-medium mb-1">Output</h5> |
| <div class="json-viewer">${JSON.stringify(node.output_json, null, 2)}</div> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
|
|
| function startNewRun() { |
| |
| alert('Starting a new SmartTriage run...'); |
| |
| } |