<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scraper MCP - Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #fafafa;
min-height: 100vh;
padding: 2rem 1.5rem;
color: #1a1a1a;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
margin-bottom: 2rem;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
}
.subtitle {
color: #737373;
font-size: 0.875rem;
font-weight: 400;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid #e5e5e5;
}
.tab {
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #737373;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.tab:hover {
color: #1a1a1a;
}
.tab.active {
color: #1a1a1a;
border-bottom-color: #1a1a1a;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
@media (max-width: 1000px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 1rem;
transition: border-color 0.2s ease;
margin-bottom: 0.75rem;
min-width: 0;
overflow: hidden;
}
.card:hover {
border-color: #d4d4d4;
}
.card:last-of-type {
margin-bottom: 0;
}
.card h2 {
font-size: 0.75rem;
font-weight: 500;
color: #737373;
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #1a1a1a;
}
.status-badge::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #22c55e;
}
.status-warning::before {
background: #f59e0b;
}
.stat {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.625rem 0;
border-bottom: 1px solid #f5f5f5;
}
.stat:last-child {
border-bottom: none;
padding-bottom: 0;
}
.stat:first-child {
padding-top: 0;
}
.stat-label {
color: #737373;
font-size: 0.875rem;
font-weight: 400;
}
.stat-value {
font-weight: 500;
color: #1a1a1a;
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
}
.big-stat {
padding: 1rem 0;
}
.big-stat-value {
font-size: 2rem;
font-weight: 600;
color: #1a1a1a;
line-height: 1;
letter-spacing: -0.025em;
font-variant-numeric: tabular-nums;
}
.big-stat-label {
color: #737373;
margin-top: 0.5rem;
font-size: 0.875rem;
font-weight: 400;
}
.request-table-container {
max-height: 320px;
overflow: auto;
}
.request-table-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.request-table-container::-webkit-scrollbar-track {
background: transparent;
}
.request-table-container::-webkit-scrollbar-thumb {
background: #e5e5e5;
border-radius: 3px;
}
.request-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.request-table th {
position: sticky;
top: 0;
background: white;
font-weight: 500;
color: #737373;
text-align: left;
padding: 0.5rem 0.5rem;
border-bottom: 1px solid #e5e5e5;
text-transform: uppercase;
font-size: 0.625rem;
letter-spacing: 0.05em;
white-space: nowrap;
}
.request-table th:first-child {
width: 90px;
}
.request-table th:nth-child(2) {
width: 60px;
}
.request-table th:nth-child(3) {
width: 80px;
}
.request-table th:nth-child(4) {
width: auto;
}
.request-table td {
padding: 0.5rem 0.5rem;
border-bottom: 1px solid #f5f5f5;
color: #1a1a1a;
}
.request-table tbody tr:hover {
background: #fafafa;
}
.request-table .time-col {
color: #737373;
white-space: nowrap;
font-variant-numeric: tabular-nums;
width: 90px;
}
.request-table .status-col {
white-space: nowrap;
font-weight: 500;
width: 60px;
}
.request-table .status-success {
color: #22c55e;
}
.request-table .status-error {
color: #ef4444;
}
.request-table .response-time-col {
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
color: #737373;
width: 80px;
}
.request-table .url-col {
width: auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.error-message {
display: block;
color: #dc2626;
font-size: 0.7rem;
margin-top: 0.25rem;
}
.refresh-indicator {
text-align: center;
color: #a3a3a3;
font-size: 0.75rem;
margin-top: 1.5rem;
font-variant-numeric: tabular-nums;
}
.loading {
opacity: 0.5;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e5e5e5;
}
.modal-header h2 {
font-size: 1rem;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #737373;
padding: 0;
line-height: 1;
}
.modal-close:hover {
color: #1a1a1a;
}
.modal-tabs {
display: flex;
gap: 0;
padding: 0 1.5rem;
border-bottom: 1px solid #e5e5e5;
}
.modal-tab {
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #737373;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
margin-bottom: -1px;
}
.modal-tab:hover {
color: #1a1a1a;
}
.modal-tab.active {
color: #1a1a1a;
border-bottom-color: #1a1a1a;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.modal-section {
margin-bottom: 1.5rem;
}
.modal-section:last-child {
margin-bottom: 0;
}
.modal-section-title {
font-size: 0.7rem;
font-weight: 500;
color: #737373;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.modal-section-content {
font-size: 0.875rem;
color: #1a1a1a;
line-height: 1.5;
}
.modal-section-content pre {
background: #f5f5f5;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-size: 0.75rem;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.modal-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.modal-stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.modal-stat-label {
font-size: 0.7rem;
font-weight: 500;
color: #737373;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.modal-stat-value {
font-size: 0.875rem;
color: #1a1a1a;
font-weight: 500;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.5rem;
border-top: 1px solid #e5e5e5;
}
.modal-footer .btn {
width: auto;
margin: 0;
}
.citation-list {
list-style: decimal;
padding-left: 1.25rem;
margin: 0;
}
.citation-list li {
margin-bottom: 0.25rem;
}
.citation-list a {
color: #2563eb;
text-decoration: none;
word-break: break-all;
}
.citation-list a:hover {
text-decoration: underline;
}
.request-table tbody tr {
cursor: pointer;
}
.request-table tbody tr:hover {
background: #f0f0f0;
}
.badge {
display: inline-block;
padding: 0.2rem 0.5rem;
font-size: 0.7rem;
font-weight: 500;
border-radius: 4px;
}
.badge-success {
background: #dcfce7;
color: #166534;
}
.badge-error {
background: #fee2e2;
color: #991b1b;
}
.badge-perplexity {
background: #e0e7ff;
color: #3730a3;
}
.badge-scraper {
background: #f0fdf4;
color: #166534;
}
.btn {
display: inline-block;
padding: 0.5rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
border: 1px solid #e5e5e5;
border-radius: 6px;
background: white;
color: #1a1a1a;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 0.75rem;
width: 100%;
}
.btn:hover {
border-color: #1a1a1a;
background: #fafafa;
}
.btn:active {
background: #f5f5f5;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form-group {
margin-bottom: 0.75rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.form-group-full {
grid-column: span 2;
}
.form-label {
display: block;
font-size: 0.7rem;
font-weight: 500;
color: #737373;
margin-bottom: 0.35rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-input {
width: 100%;
padding: 0.4rem 0.6rem;
font-size: 0.875rem;
border: 1px solid #e5e5e5;
border-radius: 6px;
background: white;
color: #1a1a1a;
transition: border-color 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #1a1a1a;
}
.form-input[type="number"] {
font-variant-numeric: tabular-nums;
}
.form-input:disabled {
background: #f5f5f5;
color: #a3a3a3;
cursor: not-allowed;
}
.form-help {
display: block;
color: #737373;
font-size: 0.7rem;
margin-top: 0.25rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.checkbox-group input[type="checkbox"] {
width: 1rem;
height: 1rem;
cursor: pointer;
}
.checkbox-group label {
font-size: 0.875rem;
font-weight: 500;
color: #1a1a1a;
cursor: pointer;
margin: 0;
text-transform: none;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 6px;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.alert-warning {
background: #fef3c7;
border: 1px solid #fcd34d;
color: #92400e;
}
/* Master-detail layout */
.master-detail {
display: grid;
grid-template-columns: 340px 1fr;
gap: 0.75rem;
min-height: 500px;
}
@media (max-width: 900px) {
.master-detail {
grid-template-columns: 1fr;
}
}
.master-panel .card {
height: 100%;
display: flex;
flex-direction: column;
}
.detail-panel {
display: flex;
flex-direction: column;
}
.detail-panel .card {
flex: 1;
display: flex;
flex-direction: column;
}
.panel-subtitle {
color: #737373;
font-size: 0.8rem;
margin-bottom: 1rem;
}
.item-list {
flex: 1;
overflow-y: auto;
max-height: 450px;
}
.item-card {
padding: 0.75rem;
border: 1px solid #e5e5e5;
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.15s ease;
}
.item-card:hover {
border-color: #d4d4d4;
background: #fafafa;
}
.item-card.selected {
border-color: #1a1a1a;
background: #f5f5f5;
}
.item-card-uri {
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.75rem;
color: #1a1a1a;
font-weight: 500;
margin-bottom: 0.25rem;
}
.item-card-name {
font-size: 0.8rem;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 0.25rem;
}
.item-card-desc {
font-size: 0.75rem;
color: #737373;
line-height: 1.4;
}
.item-card-params {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
.param-tag {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
background: #f5f5f5;
border-radius: 3px;
color: #525252;
font-family: 'SF Mono', Monaco, monospace;
}
.param-tag.required {
background: #fef3c7;
color: #92400e;
}
.category-header {
font-size: 0.65rem;
font-weight: 600;
color: #a3a3a3;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.5rem 0 0.35rem 0;
border-bottom: 1px solid #f5f5f5;
margin-bottom: 0.5rem;
}
.placeholder-text {
color: #a3a3a3;
font-size: 0.875rem;
text-align: center;
padding: 2rem 1rem;
}
.uri-badge {
display: inline-block;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.75rem;
padding: 0.35rem 0.6rem;
background: #f5f5f5;
border-radius: 4px;
color: #525252;
}
.detail-header {
margin-bottom: 0.75rem;
}
.content-pre {
flex: 1;
background: #f5f5f5;
padding: 1rem;
border-radius: 6px;
font-size: 0.75rem;
line-height: 1.5;
overflow: auto;
max-height: 400px;
white-space: pre-wrap;
word-break: break-word;
color: #1a1a1a;
margin: 0;
}
/* Playground layout */
.playground-layout {
display: grid;
grid-template-columns: 1fr 280px;
gap: 1.5rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.playground-layout {
grid-template-columns: 1fr;
}
}
.playground-input-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.playground-options-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: #f9f9f9;
border-radius: 8px;
border: 1px solid #e5e5e5;
}
.playground-settings-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.playground-btn {
margin-top: 0;
}
/* Toggle switch for JS rendering */
.js-render-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: white;
border: 1px solid #e5e5e5;
border-radius: 6px;
margin-top: 0.25rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #d4d4d4;
transition: 0.2s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.2s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.toggle-switch input:checked + .toggle-slider {
background-color: #22c55e;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.toggle-label {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.toggle-title {
font-size: 0.8rem;
font-weight: 500;
color: #1a1a1a;
}
.toggle-desc {
font-size: 0.7rem;
color: #737373;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Scraper MCP</h1>
<p class="subtitle">Web Scraping Server Dashboard</p>
</header>
<div class="tabs">
<button class="tab active" onclick="switchTab(event, 'dashboard')">Dashboard</button>
<button class="tab" onclick="switchTab(event, 'playground')">Playground</button>
<button class="tab" onclick="switchTab(event, 'resources')">Resources</button>
<button class="tab" onclick="switchTab(event, 'prompts')">Prompts</button>
<button class="tab" onclick="switchTab(event, 'config')">Config</button>
</div>
<div id="dashboard-tab" class="tab-content active">
<div class="grid">
<div class="card">
<h2>Server Status</h2>
<div class="stat">
<span class="stat-label">Status</span>
<span class="stat-value"><span id="status" class="status-badge">Healthy</span></span>
</div>
<div class="stat">
<span class="stat-label">Uptime</span>
<span class="stat-value" id="uptime">-</span>
</div>
<div class="stat">
<span class="stat-label">Started</span>
<span class="stat-value" id="start-time">-</span>
</div>
</div>
<div class="card">
<h2>Request Stats</h2>
<div class="big-stat">
<div class="big-stat-value" id="total-requests">0</div>
<div class="big-stat-label">Total Requests</div>
</div>
<div class="stat">
<span class="stat-label">Success Rate</span>
<span class="stat-value" id="success-rate">-</span>
</div>
<div class="stat">
<span class="stat-label">Failed</span>
<span class="stat-value" id="failed-requests">0</span>
</div>
</div>
<div class="card">
<h2>Retry Stats</h2>
<div class="stat">
<span class="stat-label">Total Retries</span>
<span class="stat-value" id="total-retries">0</span>
</div>
<div class="stat">
<span class="stat-label">Avg Per Request</span>
<span class="stat-value" id="avg-retries">-</span>
</div>
</div>
<div class="card">
<h2>Cache Status</h2>
<div class="stat">
<span class="stat-label">Entries</span>
<span class="stat-value" id="cache-entries">-</span>
</div>
<div class="stat">
<span class="stat-label">Size</span>
<span class="stat-value" id="cache-size">-</span>
</div>
<div class="stat">
<span class="stat-label">Hit Rate</span>
<span class="stat-value" id="cache-hit-rate">-</span>
</div>
<button class="btn" id="clear-cache-btn" onclick="clearCache()">Clear Cache</button>
</div>
</div>
<div class="card">
<h2>Recent Requests</h2>
<div class="request-table-container">
<table class="request-table">
<thead>
<tr>
<th>Time</th>
<th>Status</th>
<th class="response-time-col">Response</th>
<th>URL</th>
</tr>
</thead>
<tbody id="recent-requests">
<tr><td colspan="4" style="text-align: center; color: #737373; padding: 1rem;">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>Recent Errors (Last 10)</h2>
<div class="request-table-container">
<table class="request-table">
<thead>
<tr>
<th>Time</th>
<th>Status</th>
<th class="response-time-col">Attempts</th>
<th>URL</th>
</tr>
</thead>
<tbody id="recent-errors">
<tr><td colspan="4" style="text-align: center; color: #737373; padding: 1rem;">No errors</td></tr>
</tbody>
</table>
</div>
</div>
<div class="refresh-indicator" id="refresh-indicator">
Auto-refresh: <span id="countdown">10</span>s
</div>
</div>
<!-- Request Details Modal -->
<div id="requestModal" class="modal">
<div class="modal-overlay" onclick="closeRequestModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Request Details</h2>
<button class="modal-close" onclick="closeRequestModal()">×</button>
</div>
<div class="modal-tabs">
<button class="modal-tab active" onclick="switchModalTab(event, 'formatted')">Formatted</button>
<button class="modal-tab" onclick="switchModalTab(event, 'raw')">Raw JSON</button>
</div>
<div class="modal-body">
<div id="modal-formatted" class="modal-tab-content active">
<div id="modal-loading" style="text-align: center; padding: 2rem; color: #737373;">Loading...</div>
<div id="modal-error" style="display: none; text-align: center; padding: 2rem; color: #ef4444;"></div>
<div id="modal-details" style="display: none;"></div>
</div>
<div id="modal-raw" class="modal-tab-content" style="display: none;">
<pre id="modal-json" style="background: #f5f5f5; padding: 1rem; border-radius: 6px; font-size: 0.75rem; max-height: 500px; overflow: auto;"><code></code></pre>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="copyRequestDetails()">Copy JSON</button>
<button class="btn" onclick="closeRequestModal()">Close</button>
</div>
</div>
</div>
<div id="playground-tab" class="tab-content">
<div class="card">
<h2>API Playground</h2>
<form id="playground-form">
<div class="playground-layout">
<!-- Left column: Input and options -->
<div class="playground-input-section">
<div class="form-group" id="url-input-group">
<label class="form-label" for="test-url">URL</label>
<input type="text" id="test-url" class="form-input" placeholder="https://example.com" required>
</div>
<div class="form-group" id="query-input-group" style="display: none;">
<label class="form-label" for="test-query">Query</label>
<textarea id="test-query" class="form-input" rows="3" placeholder="What is the current state of AI in 2025?"></textarea>
</div>
<div class="form-group scraper-options">
<label class="form-label" for="test-selector">CSS Selector (optional)</label>
<input type="text" id="test-selector" class="form-input" placeholder=".article-content, #main, article">
</div>
</div>
<!-- Right column: Tool selection and settings -->
<div class="playground-options-section">
<div class="form-group">
<label class="form-label" for="test-tool">Tool</label>
<select id="test-tool" class="form-input" onchange="toggleToolInputs()">
<optgroup label="Web Scraping">
<option value="scrape_url" selected>scrape_url (Markdown)</option>
<option value="scrape_url_html">scrape_url_html (Raw HTML)</option>
<option value="scrape_url_text">scrape_url_text (Plain Text)</option>
<option value="scrape_extract_links">scrape_extract_links (Links)</option>
</optgroup>
<optgroup label="Perplexity AI">
<option value="perplexity">perplexity (Web Search)</option>
<option value="perplexity_reason">perplexity_reason (Reasoning)</option>
</optgroup>
</select>
</div>
<div class="playground-settings-row scraper-options">
<div class="form-group">
<label class="form-label" for="test-timeout">Timeout</label>
<input type="number" id="test-timeout" class="form-input" min="1" value="30">
</div>
<div class="form-group">
<label class="form-label" for="test-retries">Retries</label>
<input type="number" id="test-retries" class="form-input" min="0" value="3">
</div>
</div>
<div class="form-group perplexity-options" style="display: none;">
<label class="form-label" for="test-temperature">Temperature</label>
<input type="number" id="test-temperature" class="form-input" min="0" max="2" step="0.1" value="0.3">
</div>
<div class="js-render-toggle scraper-options">
<label class="toggle-switch">
<input type="checkbox" id="test-render-js">
<span class="toggle-slider"></span>
</label>
<div class="toggle-label">
<span class="toggle-title">JavaScript Rendering</span>
<span class="toggle-desc">Playwright for SPAs & dynamic content</span>
</div>
</div>
</div>
</div>
<button type="submit" class="btn playground-btn">Run Tool</button>
</form>
</div>
<div class="card" id="playground-response" style="display: none;">
<h2>Response</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<span id="response-status" style="font-size: 0.875rem; color: #737373;"></span>
<button onclick="copyResponse(event)" class="btn" style="width: auto; margin: 0; padding: 0.35rem 0.6rem; font-size: 0.7rem;">Copy JSON</button>
</div>
<pre id="response-json" style="background: #f5f5f5; padding: 1rem; border-radius: 6px; overflow-x: hidden; font-size: 0.75rem; line-height: 1.5; max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;"><code></code></pre>
</div>
</div>
<div id="config-tab" class="tab-content">
<div class="alert alert-warning">
⚠️ Configuration changes are not persisted and will reset when the server restarts
</div>
<div class="card">
<h2>Runtime Configuration</h2>
<form id="config-form">
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="concurrency">Concurrency</label>
<input type="number" id="concurrency" class="form-input" min="1" max="50" value="8">
<small class="form-help">Max concurrent requests (1-50)</small>
</div>
<div class="form-group">
<label class="form-label" for="cache_ttl_default">Cache TTL - Default</label>
<input type="number" id="cache_ttl_default" class="form-input" min="0" value="3600">
<small class="form-help">Default cache (1h = 3600s)</small>
</div>
<div class="form-group">
<label class="form-label" for="default_timeout">Default Timeout</label>
<input type="number" id="default_timeout" class="form-input" min="1" value="30">
<small class="form-help">Request timeout in seconds</small>
</div>
<div class="form-group">
<label class="form-label" for="cache_ttl_realtime">Cache TTL - Realtime</label>
<input type="number" id="cache_ttl_realtime" class="form-input" min="0" value="300">
<small class="form-help">API/live data cache (5m = 300s)</small>
</div>
<div class="form-group">
<label class="form-label" for="default_max_retries">Max Retries</label>
<input type="number" id="default_max_retries" class="form-input" min="0" value="3">
<small class="form-help">Max retry attempts on failure</small>
</div>
<div class="form-group">
<label class="form-label" for="cache_ttl_static">Cache TTL - Static</label>
<input type="number" id="cache_ttl_static" class="form-input" min="0" value="86400">
<small class="form-help">Static/CDN cache (24h = 86400s)</small>
</div>
</div>
<h2 style="margin-top: 1.5rem;">Proxy Settings</h2>
<div class="checkbox-group">
<input type="checkbox" id="proxy_enabled">
<label for="proxy_enabled">Enable Proxy</label>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="http_proxy">HTTP Proxy</label>
<input type="text" id="http_proxy" class="form-input" placeholder="http://proxy:8080">
<small class="form-help">HTTP proxy URL</small>
</div>
<div class="form-group">
<label class="form-label" for="https_proxy">HTTPS Proxy</label>
<input type="text" id="https_proxy" class="form-input" placeholder="https://proxy:8443">
<small class="form-help">HTTPS proxy URL</small>
</div>
<div class="form-group form-group-full">
<label class="form-label" for="no_proxy">No Proxy</label>
<input type="text" id="no_proxy" class="form-input" placeholder="localhost,127.0.0.1,.local">
<small class="form-help">Comma-separated list of hosts to bypass proxy</small>
</div>
</div>
<h2 style="margin-top: 1.5rem;">Security Settings</h2>
<div class="checkbox-group">
<input type="checkbox" id="verify_ssl">
<label for="verify_ssl">Verify SSL Certificates</label>
</div>
<div class="alert alert-warning" style="margin-top: 0.75rem;">
⚠️ SSL verification is disabled by default. Enable it for production use or when not using self-signed certificates.
</div>
<button type="submit" class="btn">Apply Changes</button>
</form>
</div>
</div>
</div>
<!-- Resources Tab -->
<div id="resources-tab" class="tab-content">
<div class="master-detail">
<div class="master-panel">
<div class="card">
<h2>MCP Resources</h2>
<p class="panel-subtitle">Read-only data endpoints</p>
<div id="resources-loading" class="loading">Loading...</div>
<div id="resources-empty" class="alert alert-warning" style="display: none;">
No resources available.
</div>
<div id="resources-list" class="item-list"></div>
</div>
</div>
<div class="detail-panel">
<div class="card">
<h2>Resource Content</h2>
<div id="resource-content-placeholder" class="placeholder-text">
Select a resource to view its content
</div>
<div id="resource-content-display" style="display: none;">
<div class="detail-header">
<code id="resource-uri-display" class="uri-badge"></code>
</div>
<pre id="resource-content" class="content-pre"></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Prompts Tab -->
<div id="prompts-tab" class="tab-content">
<div class="master-detail">
<div class="master-panel">
<div class="card">
<h2>MCP Prompts</h2>
<p class="panel-subtitle">Workflow templates</p>
<div id="prompts-loading" class="loading">Loading...</div>
<div id="prompts-empty" class="alert alert-warning" style="display: none;">
No prompts available.
</div>
<div id="prompts-list" class="item-list"></div>
</div>
</div>
<div class="detail-panel">
<div class="card" style="margin-bottom: 0.75rem;">
<h2>Parameters</h2>
<div id="prompt-params-placeholder" class="placeholder-text">
Select a prompt to configure
</div>
<form id="prompt-params-form" style="display: none;">
<div id="prompt-params-fields"></div>
<button type="submit" class="btn">Generate Prompt</button>
</form>
</div>
<div class="card">
<h2>Generated Output</h2>
<div id="prompt-output-placeholder" class="placeholder-text">
Output will appear here
</div>
<div id="prompt-output-display" style="display: none;">
<pre id="prompt-output" class="content-pre"></pre>
<button class="btn" onclick="copyPromptOutput()">Copy to Clipboard</button>
</div>
</div>
</div>
</div>
</div>
<script>
let countdown = 10;
let countdownInterval;
async function fetchStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
updateDashboard(data);
} catch (error) {
console.error('Failed to fetch stats:', error);
}
}
function updateDashboard(data) {
// Server status
document.getElementById('uptime').textContent = data.uptime.formatted;
document.getElementById('start-time').textContent = new Date(data.start_time).toLocaleString();
// Cache stats
if (data.cache && !data.cache.error) {
document.getElementById('cache-entries').textContent = data.cache.entry_count.toLocaleString();
document.getElementById('cache-size').textContent = formatBytes(data.cache.size_bytes);
const hitRate = data.cache.hits > 0
? ((data.cache.hits / (data.cache.hits + data.cache.misses)) * 100).toFixed(1) + '%'
: '0%';
document.getElementById('cache-hit-rate').textContent = hitRate;
}
// Request stats
document.getElementById('total-requests').textContent = data.requests.total.toLocaleString();
document.getElementById('success-rate').textContent = data.requests.success_rate.toFixed(1) + '%';
document.getElementById('failed-requests').textContent = data.requests.failed.toLocaleString();
// Retry stats
document.getElementById('total-retries').textContent = data.retries.total.toLocaleString();
document.getElementById('avg-retries').textContent = data.retries.average_per_request.toFixed(2);
// Recent requests
const recentRequestsEl = document.getElementById('recent-requests');
if (data.recent_requests.length > 0) {
recentRequestsEl.innerHTML = data.recent_requests.map(req => {
const statusClass = req.success ? 'status-success' : 'status-error';
const statusText = req.success ? `${req.status_code}` : `${req.status_code || 'ERR'}`;
const timestamp = new Date(req.timestamp).toLocaleTimeString();
const responseTime = req.elapsed_ms ? `${req.elapsed_ms.toFixed(0)}ms` : '-';
const typeBadge = req.request_type === 'perplexity'
? '<span class="badge badge-perplexity">AI</span>'
: '';
return `
<tr data-request-id="${req.request_id}" onclick="openRequestModal('${req.request_id}')">
<td class="time-col">${timestamp}</td>
<td class="status-col ${statusClass}">${statusText} ${typeBadge}</td>
<td class="response-time-col">${responseTime}</td>
<td class="url-col" title="${escapeHtml(req.url)}">${escapeHtml(req.url)}</td>
</tr>
`;
}).join('');
} else {
recentRequestsEl.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #737373; padding: 1rem;">No requests yet</td></tr>';
}
// Recent errors
const recentErrorsEl = document.getElementById('recent-errors');
if (data.recent_errors.length > 0) {
recentErrorsEl.innerHTML = data.recent_errors.map(err => {
const timestamp = new Date(err.timestamp).toLocaleTimeString();
const statusText = err.status_code || 'ERR';
const attempts = err.attempts > 1 ? `${err.attempts}x` : '1x';
const errorMsg = err.error ? `<span class="error-message">${escapeHtml(err.error)}</span>` : '';
return `
<tr>
<td class="time-col">${timestamp}</td>
<td class="status-col status-error">${statusText}</td>
<td class="response-time-col">${attempts}</td>
<td class="url-col" title="${escapeHtml(err.url)}">${escapeHtml(err.url)}${errorMsg}</td>
</tr>
`;
}).join('');
} else {
recentErrorsEl.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #737373; padding: 1rem;">No errors</td></tr>';
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function startCountdown() {
countdown = 10;
document.getElementById('countdown').textContent = countdown;
if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => {
countdown--;
document.getElementById('countdown').textContent = countdown;
if (countdown <= 0) {
fetchStats();
countdown = 10;
}
}, 1000);
}
async function clearCache() {
const btn = document.getElementById('clear-cache-btn');
if (!confirm('Are you sure you want to clear the cache? This will remove all cached responses.')) {
return;
}
btn.disabled = true;
btn.textContent = 'Clearing...';
try {
const response = await fetch('/api/cache/clear', {
method: 'POST'
});
if (response.ok) {
btn.textContent = 'Cleared!';
setTimeout(() => {
btn.textContent = 'Clear Cache';
btn.disabled = false;
}, 2000);
// Refresh stats immediately
fetchStats();
} else {
btn.textContent = 'Failed';
setTimeout(() => {
btn.textContent = 'Clear Cache';
btn.disabled = false;
}, 2000);
}
} catch (error) {
console.error('Failed to clear cache:', error);
btn.textContent = 'Error';
setTimeout(() => {
btn.textContent = 'Clear Cache';
btn.disabled = false;
}, 2000);
}
}
function switchTab(event, tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName + '-tab').classList.add('active');
// Load data when switching to specific tabs
if (tabName === 'config') {
loadConfig();
} else if (tabName === 'resources') {
loadResources();
} else if (tabName === 'prompts') {
loadPrompts();
}
}
// ========== SSE Response Parser ==========
function parseSSEResponse(text) {
const lines = text.trim().split('\n');
let result = null;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('data: ')) {
const data = lines[i].substring(6);
try {
const jsonrpcResponse = JSON.parse(data);
if (jsonrpcResponse.result) {
result = jsonrpcResponse.result;
} else if (jsonrpcResponse.error) {
throw new Error(jsonrpcResponse.error.message || 'Request failed');
}
} catch (e) {
if (e.message && !e.message.includes('Unexpected')) {
throw e;
}
}
}
}
return result;
}
// ========== Resources Functions ==========
let resourcesCache = null;
async function loadResources() {
if (resourcesCache) {
displayResources(resourcesCache);
return;
}
document.getElementById('resources-loading').style.display = 'block';
document.getElementById('resources-list').style.display = 'none';
document.getElementById('resources-empty').style.display = 'none';
try {
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'resources/list',
params: {}
})
});
const text = await response.text();
const result = parseSSEResponse(text);
if (result && result.resources && result.resources.length > 0) {
resourcesCache = result.resources;
displayResources(result.resources);
} else {
document.getElementById('resources-loading').style.display = 'none';
document.getElementById('resources-empty').style.display = 'block';
}
} catch (error) {
console.error('Failed to load resources:', error);
document.getElementById('resources-loading').style.display = 'none';
document.getElementById('resources-empty').style.display = 'block';
document.getElementById('resources-empty').textContent = 'Error loading resources: ' + error.message;
}
}
function displayResources(resources) {
document.getElementById('resources-loading').style.display = 'none';
document.getElementById('resources-list').style.display = 'block';
const container = document.getElementById('resources-list');
container.innerHTML = '';
// Group by URI prefix
const groups = {};
resources.forEach(resource => {
const prefix = resource.uri.split('://')[0];
if (!groups[prefix]) groups[prefix] = [];
groups[prefix].push(resource);
});
// Render grouped items
Object.keys(groups).sort().forEach(prefix => {
const header = document.createElement('div');
header.className = 'category-header';
header.textContent = prefix + '://';
container.appendChild(header);
groups[prefix].forEach(resource => {
const card = document.createElement('div');
card.className = 'item-card';
card.dataset.uri = resource.uri;
const shortDesc = (resource.description || '').split('.')[0];
card.innerHTML = `
<div class="item-card-uri">${escapeHtml(resource.uri)}</div>
<div class="item-card-desc">${escapeHtml(shortDesc)}</div>
`;
card.onclick = () => selectResource(resource.uri);
container.appendChild(card);
});
});
}
function selectResource(uri) {
document.querySelectorAll('#resources-list .item-card').forEach(c => c.classList.remove('selected'));
const card = document.querySelector(`#resources-list .item-card[data-uri="${uri}"]`);
if (card) card.classList.add('selected');
readResource(uri);
}
async function readResource(uri) {
document.getElementById('resource-content-placeholder').style.display = 'none';
document.getElementById('resource-content-display').style.display = 'block';
document.getElementById('resource-uri-display').textContent = uri;
document.getElementById('resource-content').textContent = 'Loading...';
try {
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'resources/read',
params: { uri: uri }
})
});
const text = await response.text();
const result = parseSSEResponse(text);
if (result && result.contents && result.contents.length > 0) {
const content = result.contents[0];
let displayText = content.text || JSON.stringify(content, null, 2);
// Try to pretty-print JSON
try {
const parsed = JSON.parse(displayText);
displayText = JSON.stringify(parsed, null, 2);
} catch (e) {
// Not JSON, display as-is
}
document.getElementById('resource-content').textContent = displayText;
} else {
document.getElementById('resource-content').textContent = 'No content returned';
}
} catch (error) {
console.error('Failed to read resource:', error);
document.getElementById('resource-content').textContent = 'Error: ' + error.message;
}
}
// ========== Prompts Functions ==========
let promptsCache = null;
let selectedPrompt = null;
async function loadPrompts() {
if (promptsCache) {
displayPrompts(promptsCache);
return;
}
document.getElementById('prompts-loading').style.display = 'block';
document.getElementById('prompts-list').style.display = 'none';
document.getElementById('prompts-empty').style.display = 'none';
try {
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'prompts/list',
params: {}
})
});
const text = await response.text();
const result = parseSSEResponse(text);
if (result && result.prompts && result.prompts.length > 0) {
promptsCache = result.prompts;
displayPrompts(result.prompts);
} else {
document.getElementById('prompts-loading').style.display = 'none';
document.getElementById('prompts-empty').style.display = 'block';
}
} catch (error) {
console.error('Failed to load prompts:', error);
document.getElementById('prompts-loading').style.display = 'none';
document.getElementById('prompts-empty').style.display = 'block';
document.getElementById('prompts-empty').textContent = 'Error loading prompts: ' + error.message;
}
}
function displayPrompts(prompts) {
document.getElementById('prompts-loading').style.display = 'none';
document.getElementById('prompts-list').style.display = 'block';
const container = document.getElementById('prompts-list');
container.innerHTML = '';
// Categorize prompts
const categories = {
'Analysis': ['analyze_webpage', 'summarize_content', 'extract_data', 'compare_pages'],
'SEO & Audit': ['seo_audit', 'link_audit', 'metadata_check', 'accessibility_check'],
'Research': ['research_topic', 'fact_check', 'competitive_analysis', 'news_roundup']
};
Object.keys(categories).forEach(cat => {
const catPrompts = prompts.filter(p => categories[cat].includes(p.name));
if (catPrompts.length === 0) return;
const header = document.createElement('div');
header.className = 'category-header';
header.textContent = cat;
container.appendChild(header);
catPrompts.forEach(prompt => {
const args = prompt.arguments || [];
const card = document.createElement('div');
card.className = 'item-card';
card.dataset.name = prompt.name;
const shortDesc = (prompt.description || '').split('.')[0].replace(/^[^:]+:\s*/, '');
card.innerHTML = `
<div class="item-card-name">${escapeHtml(prompt.name)}</div>
<div class="item-card-desc">${escapeHtml(shortDesc)}</div>
<div class="item-card-params">
${args.map(a => `<span class="param-tag ${a.required ? 'required' : ''}">${a.name}</span>`).join('')}
</div>
`;
card.onclick = () => selectPromptCard(prompt.name);
container.appendChild(card);
});
});
}
function selectPromptCard(promptName) {
document.querySelectorAll('#prompts-list .item-card').forEach(c => c.classList.remove('selected'));
const card = document.querySelector(`#prompts-list .item-card[data-name="${promptName}"]`);
if (card) card.classList.add('selected');
selectPrompt(promptName);
}
function selectPrompt(promptName) {
const prompt = promptsCache.find(p => p.name === promptName);
if (!prompt) return;
selectedPrompt = prompt;
document.getElementById('prompt-params-placeholder').style.display = 'none';
document.getElementById('prompt-params-form').style.display = 'block';
document.getElementById('prompt-output-placeholder').style.display = 'block';
document.getElementById('prompt-output-display').style.display = 'none';
const fieldsContainer = document.getElementById('prompt-params-fields');
fieldsContainer.innerHTML = `<div class="item-card-name" style="margin-bottom: 0.75rem;">${escapeHtml(prompt.name)}</div>`;
const args = prompt.arguments || [];
if (args.length === 0) {
fieldsContainer.innerHTML += '<p class="placeholder-text" style="padding: 0.5rem;">No parameters required</p>';
} else {
args.forEach(arg => {
const fieldHtml = `
<div class="form-group">
<label class="form-label" for="prompt-arg-${arg.name}">
${escapeHtml(arg.name)}${arg.required ? ' *' : ''}
</label>
<input type="text" class="form-input"
id="prompt-arg-${arg.name}"
name="${escapeHtml(arg.name)}"
placeholder="${escapeHtml(arg.description || '')}"
${arg.required ? 'required' : ''}>
</div>
`;
fieldsContainer.innerHTML += fieldHtml;
});
}
}
async function generatePrompt(event) {
event.preventDefault();
if (!selectedPrompt) return;
const args = {};
const promptArgs = selectedPrompt.arguments || [];
promptArgs.forEach(arg => {
const input = document.getElementById(`prompt-arg-${arg.name}`);
if (input && input.value) {
args[arg.name] = input.value;
}
});
document.getElementById('prompt-output-placeholder').style.display = 'none';
document.getElementById('prompt-output-display').style.display = 'block';
document.getElementById('prompt-output').textContent = 'Generating...';
try {
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'prompts/get',
params: {
name: selectedPrompt.name,
arguments: args
}
})
});
const text = await response.text();
const result = parseSSEResponse(text);
if (result && result.messages && result.messages.length > 0) {
const content = result.messages.map(m => {
if (typeof m.content === 'string') return m.content;
if (m.content && m.content.text) return m.content.text;
return JSON.stringify(m.content);
}).join('\n\n');
document.getElementById('prompt-output').textContent = content;
} else {
document.getElementById('prompt-output').textContent = 'No prompt content returned';
}
} catch (error) {
console.error('Failed to generate prompt:', error);
document.getElementById('prompt-output').textContent = 'Error: ' + error.message;
}
}
function copyPromptOutput() {
const content = document.getElementById('prompt-output').textContent;
navigator.clipboard.writeText(content).then(() => {
const btn = event.target;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy to Clipboard'; }, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Attach form handler
document.addEventListener('DOMContentLoaded', function() {
const promptForm = document.getElementById('prompt-params-form');
if (promptForm) {
promptForm.addEventListener('submit', generatePrompt);
}
});
function toggleProxyInputs() {
const enabled = document.getElementById('proxy_enabled').checked;
document.getElementById('http_proxy').disabled = !enabled;
document.getElementById('https_proxy').disabled = !enabled;
document.getElementById('no_proxy').disabled = !enabled;
}
function toggleToolInputs() {
const tool = document.getElementById('test-tool').value;
const isPerplexity = tool.startsWith('perplexity');
// Toggle URL vs Query inputs
document.getElementById('url-input-group').style.display = isPerplexity ? 'none' : 'block';
document.getElementById('query-input-group').style.display = isPerplexity ? 'block' : 'none';
// Toggle scraper options (timeout, retries, selector)
document.querySelectorAll('.scraper-options').forEach(el => {
el.style.display = isPerplexity ? 'none' : 'block';
});
// Toggle perplexity options (temperature)
document.querySelectorAll('.perplexity-options').forEach(el => {
el.style.display = isPerplexity ? 'block' : 'none';
});
// Update required fields
document.getElementById('test-url').required = !isPerplexity;
document.getElementById('test-query').required = isPerplexity;
}
async function loadConfig() {
try {
const response = await fetch('/api/config');
const data = await response.json();
// Populate form fields
Object.entries(data.config).forEach(([key, value]) => {
const input = document.getElementById(key);
if (input) {
if (input.type === 'checkbox') {
input.checked = value;
} else {
input.value = value;
}
}
});
// Update proxy input states
toggleProxyInputs();
} catch (error) {
console.error('Failed to load config:', error);
}
}
async function saveConfig(event) {
event.preventDefault();
const form = document.getElementById('config-form');
const formData = new FormData(form);
const config = {
concurrency: parseInt(document.getElementById('concurrency').value),
default_timeout: parseInt(document.getElementById('default_timeout').value),
default_max_retries: parseInt(document.getElementById('default_max_retries').value),
cache_ttl_default: parseInt(document.getElementById('cache_ttl_default').value),
cache_ttl_static: parseInt(document.getElementById('cache_ttl_static').value),
cache_ttl_realtime: parseInt(document.getElementById('cache_ttl_realtime').value),
proxy_enabled: document.getElementById('proxy_enabled').checked,
http_proxy: document.getElementById('http_proxy').value,
https_proxy: document.getElementById('https_proxy').value,
no_proxy: document.getElementById('no_proxy').value,
verify_ssl: document.getElementById('verify_ssl').checked,
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ config }),
});
const result = await response.json();
if (result.status === 'success') {
alert('Configuration updated successfully!');
} else {
alert('Failed to update configuration: ' + result.message);
}
} catch (error) {
console.error('Failed to save config:', error);
alert('Error saving configuration');
}
}
async function runPlaygroundTool(event) {
event.preventDefault();
const tool = document.getElementById('test-tool').value;
const isPerplexity = tool.startsWith('perplexity');
const responseCard = document.getElementById('playground-response');
const responseStatus = document.getElementById('response-status');
const responseJson = document.getElementById('response-json');
// Show loading state
responseCard.style.display = 'block';
responseStatus.textContent = 'Loading...';
responseStatus.style.color = '#737373';
responseJson.querySelector('code').textContent = '';
try {
let payload;
if (isPerplexity) {
// Perplexity tools
const query = document.getElementById('test-query').value;
const temperature = parseFloat(document.getElementById('test-temperature').value);
if (!query) {
throw new Error('Please enter a query');
}
if (tool === 'perplexity') {
payload = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: tool,
arguments: {
messages: [{ role: "user", content: query }],
temperature: temperature
}
}
};
} else {
// perplexity_reason
payload = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: tool,
arguments: {
query: query,
temperature: temperature
}
}
};
}
} else {
// Scraper tools
const url = document.getElementById('test-url').value;
const timeout = parseInt(document.getElementById('test-timeout').value);
const maxRetries = parseInt(document.getElementById('test-retries').value);
const cssSelector = document.getElementById('test-selector').value;
const renderJs = document.getElementById('test-render-js').checked;
payload = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: tool,
arguments: {
urls: [url],
timeout: timeout,
max_retries: maxRetries,
render_js: renderJs
}
}
};
// Add optional css_selector
if (cssSelector) {
payload.params.arguments.css_selector = cssSelector;
}
}
const startTime = Date.now();
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify(payload)
});
const elapsed = Date.now() - startTime;
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const text = await response.text();
// Parse SSE response - extract JSON from data: line
const lines = text.trim().split('\n');
let result = null;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('data: ')) {
const data = lines[i].substring(6);
try {
const jsonrpcResponse = JSON.parse(data);
// Extract the actual tool result from JSON-RPC response
if (jsonrpcResponse.result && jsonrpcResponse.result.structuredContent) {
result = jsonrpcResponse.result.structuredContent;
} else if (jsonrpcResponse.result) {
result = jsonrpcResponse.result;
} else if (jsonrpcResponse.error) {
throw new Error(jsonrpcResponse.error.message || 'Tool call failed');
}
} catch (e) {
if (e.message && !e.message.includes('Unexpected')) {
throw e;
}
// Skip invalid JSON
}
}
}
if (result) {
responseStatus.textContent = `Completed in ${elapsed}ms`;
responseJson.querySelector('code').textContent = JSON.stringify(result, null, 2);
} else {
// Debug: show raw response if parsing failed
console.error('Failed to parse response:', text);
throw new Error('No valid response received. Check browser console for raw response.');
}
} catch (error) {
responseStatus.textContent = 'Error';
responseStatus.style.color = '#ef4444';
responseJson.querySelector('code').textContent = JSON.stringify({
error: error.message,
details: error.toString()
}, null, 2);
}
}
function copyResponse(event) {
const code = document.getElementById('response-json').querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
// Initial load
fetchStats();
startCountdown();
// Setup config form handler
document.getElementById('config-form').addEventListener('submit', saveConfig);
// Setup proxy checkbox handler
document.getElementById('proxy_enabled').addEventListener('change', toggleProxyInputs);
// Setup playground form handler
document.getElementById('playground-form').addEventListener('submit', runPlaygroundTool);
// Modal state
let currentRequestData = null;
async function openRequestModal(requestId) {
const modal = document.getElementById('requestModal');
const loading = document.getElementById('modal-loading');
const error = document.getElementById('modal-error');
const details = document.getElementById('modal-details');
const jsonEl = document.getElementById('modal-json').querySelector('code');
// Show modal and loading state
modal.classList.add('active');
loading.style.display = 'block';
error.style.display = 'none';
details.style.display = 'none';
try {
const response = await fetch(`/api/requests/${requestId}/details`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to load request details');
}
currentRequestData = data;
// Render raw JSON
jsonEl.textContent = JSON.stringify(data, null, 2);
// Render formatted view
renderFormattedDetails(data);
loading.style.display = 'none';
details.style.display = 'block';
} catch (err) {
loading.style.display = 'none';
error.style.display = 'block';
error.textContent = err.message;
}
}
function closeRequestModal() {
document.getElementById('requestModal').classList.remove('active');
currentRequestData = null;
}
function switchModalTab(event, tabName) {
// Update tab buttons
document.querySelectorAll('.modal-tab').forEach(tab => tab.classList.remove('active'));
event.target.classList.add('active');
// Update tab content
document.getElementById('modal-formatted').style.display = tabName === 'formatted' ? 'block' : 'none';
document.getElementById('modal-raw').style.display = tabName === 'raw' ? 'block' : 'none';
}
function copyRequestDetails() {
if (currentRequestData) {
navigator.clipboard.writeText(JSON.stringify(currentRequestData, null, 2)).then(() => {
alert('Copied to clipboard!');
});
}
}
function renderFormattedDetails(data) {
const detailsEl = document.getElementById('modal-details');
const isPerplexity = data.request_type === 'perplexity';
let html = `
<div class="modal-section">
<div class="modal-section-title">Overview</div>
<div class="modal-grid">
<div class="modal-stat">
<span class="modal-stat-label">Type</span>
<span class="modal-stat-value">
<span class="badge ${isPerplexity ? 'badge-perplexity' : 'badge-scraper'}">
${isPerplexity ? 'Perplexity AI' : 'Web Scraper'}
</span>
</span>
</div>
<div class="modal-stat">
<span class="modal-stat-label">Status</span>
<span class="modal-stat-value">
<span class="badge ${data.success ? 'badge-success' : 'badge-error'}">
${data.success ? 'Success' : 'Failed'}
</span>
${data.status_code ? ` (${data.status_code})` : ''}
</span>
</div>
<div class="modal-stat">
<span class="modal-stat-label">Timestamp</span>
<span class="modal-stat-value">${new Date(data.timestamp).toLocaleString()}</span>
</div>
<div class="modal-stat">
<span class="modal-stat-label">Duration</span>
<span class="modal-stat-value">${data.elapsed_ms ? data.elapsed_ms + 'ms' : '-'}</span>
</div>
</div>
</div>
`;
// URL/Target
html += `
<div class="modal-section">
<div class="modal-section-title">${isPerplexity ? 'Target' : 'URL'}</div>
<div class="modal-section-content" style="word-break: break-all;">
${escapeHtml(data.url)}
</div>
</div>
`;
// Error if present
if (data.error) {
html += `
<div class="modal-section">
<div class="modal-section-title">Error</div>
<div class="modal-section-content" style="color: #ef4444;">
${escapeHtml(data.error)}
</div>
</div>
`;
}
if (isPerplexity && data.perplexity) {
// Perplexity-specific details
const p = data.perplexity;
if (p.model) {
html += `
<div class="modal-section">
<div class="modal-section-title">Model</div>
<div class="modal-section-content">${escapeHtml(p.model)}</div>
</div>
`;
}
if (p.full_prompt) {
html += `
<div class="modal-section">
<div class="modal-section-title">Prompt</div>
<div class="modal-section-content">
<pre>${escapeHtml(p.full_prompt)}</pre>
</div>
</div>
`;
}
if (p.content) {
const content = p.content.length > 3000
? p.content.substring(0, 3000) + '\n\n... [truncated]'
: p.content;
html += `
<div class="modal-section">
<div class="modal-section-title">Response</div>
<div class="modal-section-content">
<pre>${escapeHtml(content)}</pre>
</div>
</div>
`;
}
if (p.citations && p.citations.length > 0) {
html += `
<div class="modal-section">
<div class="modal-section-title">Citations (${p.citations.length})</div>
<div class="modal-section-content">
<ol class="citation-list">
${p.citations.map(c => `<li><a href="${escapeHtml(c)}" target="_blank">${escapeHtml(c)}</a></li>`).join('')}
</ol>
</div>
</div>
`;
}
if (p.usage && Object.keys(p.usage).length > 0) {
html += `
<div class="modal-section">
<div class="modal-section-title">Token Usage</div>
<div class="modal-grid" style="grid-template-columns: repeat(3, 1fr);">
${p.usage.prompt_tokens !== undefined ? `
<div class="modal-stat">
<span class="modal-stat-label">Prompt</span>
<span class="modal-stat-value">${p.usage.prompt_tokens.toLocaleString()}</span>
</div>
` : ''}
${p.usage.completion_tokens !== undefined ? `
<div class="modal-stat">
<span class="modal-stat-label">Completion</span>
<span class="modal-stat-value">${p.usage.completion_tokens.toLocaleString()}</span>
</div>
` : ''}
${p.usage.total_tokens !== undefined ? `
<div class="modal-stat">
<span class="modal-stat-label">Total</span>
<span class="modal-stat-value">${p.usage.total_tokens.toLocaleString()}</span>
</div>
` : ''}
</div>
</div>
`;
}
} else if (!isPerplexity) {
// Scraper-specific details
if (data.attempts && data.attempts > 1) {
html += `
<div class="modal-section">
<div class="modal-section-title">Retry Info</div>
<div class="modal-section-content">
${data.attempts} attempts
</div>
</div>
`;
}
if (data.cache_expired) {
html += `
<div class="modal-section">
<div class="modal-section-title">Cache Status</div>
<div class="modal-section-content" style="color: #f59e0b;">
Content no longer cached. The cached response has expired or been evicted.
</div>
</div>
`;
} else if (data.cached_content) {
const content = typeof data.cached_content === 'string'
? data.cached_content
: JSON.stringify(data.cached_content, null, 2);
const displayContent = content.length > 5000
? content.substring(0, 5000) + '\n\n... [truncated]'
: content;
html += `
<div class="modal-section">
<div class="modal-section-title">Cached Content ${data.content_truncated ? `(${data.full_content_length.toLocaleString()} chars total)` : ''}</div>
<div class="modal-section-content">
<pre>${escapeHtml(displayContent)}</pre>
</div>
</div>
`;
} else if (data.details_unavailable) {
html += `
<div class="modal-section">
<div class="modal-section-title">Content</div>
<div class="modal-section-content" style="color: #737373;">
${escapeHtml(data.details_unavailable)}
</div>
</div>
`;
}
}
detailsEl.innerHTML = html;
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeRequestModal();
}
});
</script>
</body>
</html>