diff --git a/apps/api/static/app.js b/apps/api/static/app.js index 730a1f6..9237403 100644 --- a/apps/api/static/app.js +++ b/apps/api/static/app.js @@ -59,6 +59,13 @@ const elements = { // Opik Trace Link (PR 4.1) traceSection: document.getElementById('trace-section'), traceLink: document.getElementById('trace-link'), + traceLogBtn: document.getElementById('trace-log-btn'), + traceLogModal: document.getElementById('trace-log-modal'), + + // Terminal Console (Technical Logs) + terminalConsole: document.getElementById('terminal-console'), + terminalOutput: document.getElementById('terminal-output'), + terminalStatus: document.querySelector('.terminal-status'), }; // ========================================================================== @@ -251,6 +258,229 @@ function renderTraceLink(enabled, traceUrl = null) { } } +/** + * Enables or disables the Opik trace log button and manages modal display. + * @param {boolean} enabled - Whether the trace log button should be clickable + * @param {Object|null} logData - Optional log data object for rendering in the modal + */ +function renderTraceLogButton(enabled, logData = null) { + const btn = elements.traceLogBtn; + const modal = elements.traceLogModal; + if (!btn || !modal) return; + + if (!enabled) { + btn.classList.add('btn-trace--disabled'); + btn.setAttribute('aria-disabled', 'true'); + btn.onclick = null; + modal.classList.add('trace-log-modal--hidden'); + modal.classList.remove('trace-log-modal--visible'); + modal.innerHTML = ''; + return; + } + + btn.classList.remove('btn-trace--disabled'); + btn.setAttribute('aria-disabled', 'false'); + btn.onclick = () => { + if (modal.classList.contains('trace-log-modal--visible')) { + modal.classList.remove('trace-log-modal--visible'); + modal.classList.add('trace-log-modal--hidden'); + return; + } + // Render log content + modal.innerHTML = ` +
System Trace ID:
+
${logData?.trace_id ?? '—'}
+
Model:
+
gpt-4o (Planner)
+
Validation Engine:
+
PlanProof-Py-Validator v1.2
+
Calculated Recall:
+
${typeof logData?.recall === 'number' ? (logData.recall * 100).toFixed(1) + '%' : '—'}
+
Repair Attempted:
+
${logData?.repair_attempted ? 'Yes' : 'No'}
+ `; + modal.classList.remove('trace-log-modal--hidden'); + modal.classList.add('trace-log-modal--visible'); + }; +} + +// ========================================================================== +// Terminal Console (Technical Logs) +// ========================================================================== + +let terminalAnimationTimer = null; + +/** + * Resets the terminal console to its initial state. + */ +function resetTerminalConsole() { + const console = elements.terminalConsole; + const output = elements.terminalOutput; + const status = elements.terminalStatus; + + if (terminalAnimationTimer) { + clearTimeout(terminalAnimationTimer); + terminalAnimationTimer = null; + } + + if (console) { + console.classList.remove('terminal-console--processing', 'terminal-console--complete', 'terminal-console--error'); + } + + if (output) { + output.innerHTML = 'Awaiting system diagnostics...'; + } + + if (status) { + status.textContent = 'READY'; + } +} + +/** + * Sets the terminal to processing state. + */ +function setTerminalProcessing() { + const console = elements.terminalConsole; + const output = elements.terminalOutput; + const status = elements.terminalStatus; + + if (console) { + console.classList.remove('terminal-console--complete', 'terminal-console--error'); + console.classList.add('terminal-console--processing'); + } + + if (output) { + output.innerHTML = ''; + } + + if (status) { + status.textContent = 'PROCESSING'; + } +} + +/** + * Renders technical logs with a typing animation effect. + * Each log line appears one-by-one with a 300ms delay. + * Lines starting with [OPIK] become clickable links to the Opik trace. + * @param {string[]|null} logs - Array of log strings from technical_logs + * @param {string|null} traceId - The trace ID for building Opik links + */ +function renderTerminalLogs(logs, traceId = null) { + const console = elements.terminalConsole; + const output = elements.terminalOutput; + const status = elements.terminalStatus; + + // Clear any existing animation + if (terminalAnimationTimer) { + clearTimeout(terminalAnimationTimer); + terminalAnimationTimer = null; + } + + // Handle empty logs + if (!logs || logs.length === 0) { + if (console) { + console.classList.remove('terminal-console--processing'); + console.classList.add('terminal-console--complete'); + } + if (output) { + output.innerHTML = 'No diagnostic logs available.'; + } + if (status) { + status.textContent = 'COMPLETE'; + } + return; + } + + // Start with empty output and cursor + if (output) { + output.innerHTML = ''; + } + + let currentIndex = 0; + + /** + * Animates the next log line. + */ + function animateNextLine() { + if (currentIndex >= logs.length) { + // Animation complete + if (console) { + console.classList.remove('terminal-console--processing'); + console.classList.add('terminal-console--complete'); + } + if (status) { + status.textContent = 'COMPLETE'; + } + // Add final cursor + if (output) { + const cursor = document.createElement('span'); + cursor.className = 'terminal-cursor'; + output.appendChild(cursor); + } + return; + } + + const logText = logs[currentIndex]; + const lineElement = document.createElement('span'); + lineElement.className = 'terminal-line'; + + // Check if this is an Opik log line + if (logText.startsWith('[OPIK]') && traceId) { + lineElement.classList.add('terminal-line--opik'); + lineElement.textContent = logText; + lineElement.title = 'Click to view trace in Opik'; + lineElement.onclick = () => { + const opikUrl = `https://www.comet.com/opik/silviu-druma/projects/Hackaton/traces/${traceId}`; + window.open(opikUrl, '_blank', 'noopener,noreferrer'); + }; + } else { + lineElement.textContent = logText; + } + + if (output) { + output.appendChild(lineElement); + // Auto-scroll to bottom + output.scrollTop = output.scrollHeight; + } + + currentIndex++; + + // Schedule next line with 300ms delay + terminalAnimationTimer = setTimeout(animateNextLine, 300); + } + + // Start the animation + animateNextLine(); +} + +/** + * Sets the terminal to error state. + * @param {string} message - Error message to display + */ +function setTerminalError(message) { + const console = elements.terminalConsole; + const output = elements.terminalOutput; + const status = elements.terminalStatus; + + if (terminalAnimationTimer) { + clearTimeout(terminalAnimationTimer); + terminalAnimationTimer = null; + } + + if (console) { + console.classList.remove('terminal-console--processing', 'terminal-console--complete'); + console.classList.add('terminal-console--error'); + } + + if (output) { + output.innerHTML = `[ERROR] ${escapeHtml(message)}`; + } + + if (status) { + status.textContent = 'ERROR'; + } +} + // ========================================================================== // Metrics Grid Rendering (PR 1.4) // ========================================================================== @@ -591,14 +821,19 @@ function resetInsights() { * Renders the full validation state (status badge, checklist, metrics grid, coverage, errors). * @param {Object|null} validation - The validation object from API response */ -function renderValidation(validation) { +function renderValidation(validation, traceId = null) { if (!validation) { renderStatusBadge('pending'); renderChecklist(null); renderMetricsGrid(null); renderCoverage(null); renderErrors([]); - renderTraceLink(false); + // Show trace link after validation is rendered (PR 4.1) + let traceUrl = null; + if (traceId) { + traceUrl = `https://www.comet.com/opik/silviu-druma/projects/Hackaton/traces/${traceId}`; + } + renderTraceLink(true, traceUrl); return; } @@ -623,6 +858,7 @@ function renderValidation(validation) { // Show trace link after validation is rendered (PR 4.1) renderTraceLink(true); + renderTraceLogButton(false); } /** @@ -827,20 +1063,20 @@ function showLoadingState() { btn.dataset.originalText = btn.textContent; btn.textContent = 'Analyzing Context...'; } - + // Show loading spinner in timeline const loading = elements.timelineLoading; const empty = document.querySelector('.timeline-empty'); if (loading) loading.classList.add('timeline-loading--active'); if (empty) empty.style.display = 'none'; - + // Clear existing timeline items const container = elements.timelineContainer; if (container) { const items = container.querySelectorAll('.timeline-item'); items.forEach(item => item.remove()); } - + // Cycle through loading messages let msgIndex = 0; if (elements.loadingMessage) { @@ -850,6 +1086,9 @@ function showLoadingState() { elements.loadingMessage.textContent = LOADING_MESSAGES[msgIndex]; }, 2000); } + + // Set terminal to processing state + setTerminalProcessing(); } /** @@ -958,7 +1197,8 @@ async function generatePlan() { // Render the response renderTimeline(data.plan || [], isRejected); - renderValidation(data.validation || null); + const traceId = data?.debug?.trace_id; + renderValidation(data.validation || null, traceId); // Render extracted metadata (PR 2.1) const constraints = data.extracted_metadata?.detected_constraints || []; @@ -970,17 +1210,24 @@ async function generatePlan() { // Render assumptions & questions (PR 3.4) renderAssumptions(data.assumptions || null); renderQuestions(data.questions || null); - + + // Render terminal console with technical logs + const technicalLogs = data.technical_logs || data.debug?.technical_logs || []; + renderTerminalLogs(technicalLogs, traceId); + // Hide loading state (PR 3.1) hideLoadingState(); } catch (error) { console.error('Failed to generate plan:', error); hideLoadingState(); - + // Show friendly error in sidebar (PR 3.2) renderApiError(error.message); - + + // Set terminal to error state + setTerminalError(error.message); + // Reset timeline to empty state resetTimeline(); } @@ -1014,6 +1261,11 @@ window.PlanProof = { renderTraceLink, generatePlan, formatTime, + // Terminal Console + resetTerminalConsole, + setTerminalProcessing, + renderTerminalLogs, + setTerminalError, }; // ========================================================================== @@ -1027,6 +1279,7 @@ document.addEventListener('DOMContentLoaded', () => { resetCoverage(); resetRepairLog(); resetInsights(); + resetTerminalConsole(); // Set default current time to now const currentTimeInput = elements.currentTime; diff --git a/apps/api/static/index.html b/apps/api/static/index.html index b8ee1a7..2b8df75 100644 --- a/apps/api/static/index.html +++ b/apps/api/static/index.html @@ -237,7 +237,7 @@

Errors

-
+ + + + +
+ + +
+
+ System Diagnostics + READY +
+
+ +
diff --git a/apps/api/static/styles.css b/apps/api/static/styles.css index 6cddbb4..df2c66c 100644 --- a/apps/api/static/styles.css +++ b/apps/api/static/styles.css @@ -1257,6 +1257,234 @@ body { opacity: 0.7; } +/* -------------------------------------------------------------------------- + Trace Log Modal (PR 4.2) + -------------------------------------------------------------------------- */ +.trace-log-modal { + display: none; + position: relative; + margin-top: var(--space-md); + background: #0a0a0a; + color: #a3e635; + font-family: var(--font-system); + font-size: 0.85rem; + border-radius: var(--radius-md); + box-shadow: 0 2px 16px rgba(0,0,0,0.7); + padding: var(--space-lg); + white-space: pre-line; + z-index: 100; + min-height: 120px; + max-width: 100%; + word-break: break-word; +} + +.trace-log-modal--visible { + display: block; +} + +.trace-log-modal--hidden { + display: none; +} + +.trace-log-modal .trace-log-label { + color: #a3e635; + font-weight: bold; + letter-spacing: 0.05em; +} + +.trace-log-modal .trace-log-value { + color: #e0e0e0; + font-family: var(--font-system); + font-weight: 400; +} + +/* -------------------------------------------------------------------------- + Terminal Console (Technical Logs) + -------------------------------------------------------------------------- */ +.terminal-console { + margin-top: var(--space-lg); + background-color: #0a0a0a; + border: 1px solid #1a1a1a; + border-radius: var(--radius-md); + overflow: hidden; + position: relative; +} + +/* Scan-line overlay effect */ +.terminal-console::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.15), + rgba(0, 0, 0, 0.15) 1px, + transparent 1px, + transparent 2px + ); + pointer-events: none; + z-index: 2; +} + +/* CRT glow effect */ +.terminal-console::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + ellipse at center, + rgba(16, 185, 129, 0.03) 0%, + transparent 70% + ); + pointer-events: none; + z-index: 1; +} + +.terminal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-xs) var(--space-sm); + background-color: #141414; + border-bottom: 1px solid #1a1a1a; +} + +.terminal-title { + font-family: var(--font-system); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #10b981; +} + +.terminal-status { + font-family: var(--font-system); + font-size: 0.5625rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #10b981; + animation: status-blink 2s ease-in-out infinite; +} + +@keyframes status-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.terminal-output { + padding: var(--space-sm); + min-height: 100px; + max-height: 200px; + overflow-y: auto; + font-family: var(--font-system); + font-size: 0.6875rem; + line-height: 1.6; + color: #10b981; + position: relative; + z-index: 3; +} + +/* Custom scrollbar for terminal */ +.terminal-output::-webkit-scrollbar { + width: 4px; +} + +.terminal-output::-webkit-scrollbar-track { + background: #0a0a0a; +} + +.terminal-output::-webkit-scrollbar-thumb { + background: #1a1a1a; + border-radius: 2px; +} + +.terminal-output::-webkit-scrollbar-thumb:hover { + background: #2a2a2a; +} + +/* Terminal log lines */ +.terminal-line { + display: block; + padding: 1px 0; + opacity: 0; + animation: line-appear 0.2s ease-out forwards; +} + +@keyframes line-appear { + from { + opacity: 0; + transform: translateX(-4px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Opik trace links */ +.terminal-line--opik { + color: #0ea5e9; + cursor: pointer; + transition: color var(--transition-fast), text-shadow var(--transition-fast); +} + +.terminal-line--opik:hover { + color: #38bdf8; + text-shadow: 0 0 8px rgba(14, 165, 233, 0.5); + text-decoration: underline; +} + +/* Blinking cursor */ +.terminal-cursor { + display: inline-block; + width: 6px; + height: 12px; + background-color: #10b981; + animation: cursor-blink 1s step-end infinite; + vertical-align: middle; + margin-left: 2px; +} + +@keyframes cursor-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* Terminal states */ +.terminal-console--processing .terminal-status { + color: #f59e0b; + animation: status-pulse 0.5s ease-in-out infinite; +} + +@keyframes status-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +.terminal-console--complete .terminal-status { + color: #10b981; + animation: none; +} + +.terminal-console--error .terminal-status { + color: #f43f5e; + animation: none; +} + +/* Empty state message */ +.terminal-empty { + color: #3f3f46; + font-style: italic; +} + /* -------------------------------------------------------------------------- Utility Classes -------------------------------------------------------------------------- */