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
-
+
+
+
+
+
+
+
+
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
-------------------------------------------------------------------------- */