tomaszpanat's picture
Directory Structure:
9d7315a verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Perplexity API Explorer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.api-key-input {
letter-spacing: 0.3em;
}
.result-card {
transition: all 0.3s ease;
}
.result-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.query-item {
transition: all 0.2s ease;
}
.query-item:hover {
background-color: #f8fafc;
}
.model-badge {
transition: all 0.2s ease;
}
.model-badge:hover {
transform: scale(1.05);
}
.loading-bar {
height: 3px;
background: linear-gradient(90deg, #6366f1, #8b5cf6, #ec4899);
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% { width: 0%; }
50% { width: 100%; }
100% { width: 0%; }
}
</style>
</head>
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
<div id="app" class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-12 text-center">
<h1 class="text-4xl md:text-5xl font-bold text-gray-800 mb-4">Perplexity API Explorer</h1>
<p class="text-lg text-gray-600 max-w-3xl mx-auto">Test and explore the full capabilities of the Perplexity Search API with real-time results and performance metrics</p>
</header>
<!-- API Key Section -->
<section class="bg-white rounded-2xl shadow-lg p-6 mb-8">
<div class="flex items-center mb-4">
<i class="fas fa-key text-indigo-600 text-xl mr-3"></i>
<h2 class="text-2xl font-bold text-gray-800">API Configuration</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="api-key">
Perplexity API Key
</label>
<div class="relative">
<input
id="api-key"
type="password"
placeholder="Enter your PPLX API key"
class="api-key-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
<button id="toggle-key" class="absolute right-3 top-3 text-gray-500">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="endpoint">
API Endpoint
</label>
<input
id="endpoint"
type="text"
value="https://api.perplexity.ai/search"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
readonly
>
</div>
</div>
</section>
<!-- Query Builder -->
<section class="bg-white rounded-2xl shadow-lg p-6 mb-8">
<div class="flex items-center mb-6">
<i class="fas fa-search text-indigo-600 text-xl mr-3"></i>
<h2 class="text-2xl font-bold text-gray-800">Query Builder</h2>
</div>
<div class="mb-6">
<div class="flex flex-wrap gap-2 mb-4">
<button id="add-query" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition">
<i class="fas fa-plus mr-2"></i>Add Query
</button>
<button id="clear-queries" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition">
<i class="fas fa-trash mr-2"></i>Clear All
</button>
<button id="run-queries" class="px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition">
<i class="fas fa-bolt mr-2"></i>Run Queries
</button>
</div>
<div id="queries-container" class="space-y-4">
<!-- Query items will be added here dynamically -->
</div>
</div>
</section>
<!-- Results Section -->
<section id="results-section" class="hidden">
<div class="flex items-center mb-6">
<i class="fas fa-chart-bar text-indigo-600 text-xl mr-3"></i>
<h2 class="text-2xl font-bold text-gray-800">Results & Performance</h2>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl shadow-lg p-6 text-white">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">Total Queries</h3>
<i class="fas fa-list text-2xl"></i>
</div>
<p id="total-queries" class="text-3xl font-bold mt-4">0</p>
</div>
<div class="bg-gradient-to-br from-green-500 to-teal-600 rounded-2xl shadow-lg p-6 text-white">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">Avg. Response Time</h3>
<i class="fas fa-tachometer-alt text-2xl"></i>
</div>
<p id="avg-response-time" class="text-3xl font-bold mt-4">0ms</p>
</div>
<div class="bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl shadow-lg p-6 text-white">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">Success Rate</h3>
<i class="fas fa-check-circle text-2xl"></i>
</div>
<p id="success-rate" class="text-3xl font-bold mt-4">0%</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-lg p-6 mb-8">
<h3 class="text-xl font-bold text-gray-800 mb-4">Response Time Distribution</h3>
<canvas id="response-time-chart" height="100"></canvas>
</div>
<div id="results-container" class="space-y-6">
<!-- Results will be added here dynamically -->
</div>
</section>
</div>
<!-- Loading Indicator -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-2xl p-8 max-w-md w-full mx-4 text-center">
<div class="loading-bar rounded-full mb-6"></div>
<h3 class="text-2xl font-bold text-gray-800 mb-2">Processing Queries</h3>
<p class="text-gray-600 mb-4">Sending requests to Perplexity API...</p>
<div id="progress-text" class="text-indigo-600 font-medium">0/0 queries completed</div>
</div>
</div>
<script>
// DOM Elements
const apiKeyInput = document.getElementById('api-key');
const toggleKeyBtn = document.getElementById('toggle-key');
const endpointInput = document.getElementById('endpoint');
const addQueryBtn = document.getElementById('add-query');
const clearQueriesBtn = document.getElementById('clear-queries');
const runQueriesBtn = document.getElementById('run-queries');
const queriesContainer = document.getElementById('queries-container');
const resultsSection = document.getElementById('results-section');
const resultsContainer = document.getElementById('results-container');
const loadingOverlay = document.getElementById('loading-overlay');
const progressText = document.getElementById('progress-text');
const totalQueriesEl = document.getElementById('total-queries');
const avgResponseTimeEl = document.getElementById('avg-response-time');
const successRateEl = document.getElementById('success-rate');
const responseTimeChartCtx = document.getElementById('response-time-chart').getContext('2d');
// State
let queryCounter = 0;
let resultsData = [];
let responseTimeChart = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Toggle API key visibility
toggleKeyBtn.addEventListener('click', () => {
const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
apiKeyInput.setAttribute('type', type);
toggleKeyBtn.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
});
// Add query button
addQueryBtn.addEventListener('click', addQueryItem);
// Clear queries button
clearQueriesBtn.addEventListener('click', clearQueries);
// Run queries button
runQueriesBtn.addEventListener('click', runQueries);
// Add initial query
addQueryItem();
});
// Add a new query item
function addQueryItem() {
queryCounter++;
const queryId = `query-${queryCounter}`;
const queryItem = document.createElement('div');
queryItem.className = 'query-item bg-gray-50 rounded-xl p-5 border border-gray-200';
queryItem.id = queryId;
queryItem.innerHTML = `
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">Query #${queryCounter}</h3>
<button class="remove-query text-gray-500 hover:text-red-500" data-id="${queryId}">
<i class="fas fa-times"></i>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">Search Query</label>
<input
type="text"
placeholder="Enter your search query"
class="query-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
data-id="${queryId}"
>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">Model</label>
<select class="model-select w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" data-id="${queryId}">
<option value="sonar-pro">Sonar Pro</option>
<option value="sonar-small">Sonar Small</option>
<option value="sonar-medium">Sonar Medium</option>
<option value="sonar-large">Sonar Large</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">Country Filter (ISO)</label>
<input
type="text"
placeholder="e.g., US, GB, DE"
class="country-filter w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
data-id="${queryId}"
>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">Max Tokens Per Page</label>
<input
type="number"
min="256"
max="2048"
value="1024"
class="max-tokens w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
data-id="${queryId}"
>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">Recency (days)</label>
<input
type="number"
min="1"
max="365"
value="30"
class="recency-filter w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
data-id="${queryId}"
>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">Domain Filter (comma separated)</label>
<input
type="text"
placeholder="e.g., wikipedia.org, github.com"
class="domain-filter w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
data-id="${queryId}"
>
</div>
`;
queriesContainer.appendChild(queryItem);
// Add event listener for remove button
queryItem.querySelector('.remove-query').addEventListener('click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
document.getElementById(id).remove();
});
}
// Clear all queries
function clearQueries() {
queriesContainer.innerHTML = '';
queryCounter = 0;
addQueryItem();
}
// Run all queries
async function runQueries() {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
alert('Please enter your Perplexity API key');
return;
}
const queryItems = document.querySelectorAll('.query-item');
if (queryItems.length === 0) {
alert('Please add at least one query');
return;
}
// Show loading overlay
loadingOverlay.classList.remove('hidden');
progressText.textContent = `0/${queryItems.length} queries completed`;
// Reset results
resultsContainer.innerHTML = '';
resultsData = [];
// Process queries in batches of 5 (API limit)
const batchSize = 5;
const batches = [];
for (let i = 0; i < queryItems.length; i += batchSize) {
batches.push(Array.from(queryItems).slice(i, i + batchSize));
}
let completed = 0;
const startTime = Date.now();
// Process each batch
for (const batch of batches) {
const batchPromises = batch.map(item => processQuery(item, apiKey));
const batchResults = await Promise.all(batchPromises);
completed += batch.length;
progressText.textContent = `${completed}/${queryItems.length} queries completed`;
// Add results to data
resultsData.push(...batchResults);
// Add results to DOM
batchResults.forEach(result => {
addResultToDOM(result);
});
}
// Hide loading overlay
loadingOverlay.classList.add('hidden');
// Show results section
resultsSection.classList.remove('hidden');
// Update metrics
updateMetrics(resultsData, Date.now() - startTime);
// Render chart
renderResponseTimeChart();
}
// Process a single query
async function processQuery(queryItem, apiKey) {
const queryId = queryItem.id;
const queryInput = queryItem.querySelector('.query-input');
const modelSelect = queryItem.querySelector('.model-select');
const countryFilter = queryItem.querySelector('.country-filter');
const maxTokens = queryItem.querySelector('.max-tokens');
const recencyFilter = queryItem.querySelector('.recency-filter');
const domainFilter = queryItem.querySelector('.domain-filter');
const query = queryInput.value.trim();
const model = modelSelect.value;
const country = countryFilter.value.trim();
const tokens = parseInt(maxTokens.value) || 1024;
const recency = parseInt(recencyFilter.value) || 30;
const domains = domainFilter.value.split(',').map(d => d.trim()).filter(d => d);
if (!query) {
return {
id: queryId,
query,
error: 'Query cannot be empty',
time: 0
};
}
const startTime = Date.now();
try {
// Prepare request data
const requestData = {
query,
model,
search_domain_filter: domains,
date_filter: `${recency}d`,
max_tokens_per_page: tokens
};
if (country) {
requestData.country_filter = country;
}
// Make API request
const response = await fetch(endpointInput.value, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const endTime = Date.now();
const responseTime = endTime - startTime;
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
id: queryId,
query,
model,
error: `API Error: ${response.status} - ${errorData.message || response.statusText}`,
time: responseTime
};
}
const data = await response.json();
return {
id: queryId,
query,
model,
data,
time: responseTime
};
} catch (error) {
const endTime = Date.now();
return {
id: queryId,
query,
model,
error: `Network Error: ${error.message}`,
time: endTime - startTime
};
}
}
// Add result to DOM
function addResultToDOM(result) {
const resultCard = document.createElement('div');
resultCard.className = 'result-card bg-white rounded-2xl shadow-lg overflow-hidden';
if (result.error) {
resultCard.innerHTML = `
<div class="p-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center mr-3">
<i class="fas fa-exclamation-triangle text-red-600"></i>
</div>
<h3 class="text-xl font-bold text-gray-800">${result.query || 'Unnamed Query'}</h3>
</div>
<div class="bg-red-50 border-l-4 border-red-500 p-4">
<p class="text-red-700">${result.error}</p>
</div>
<div class="mt-4 text-sm text-gray-500">
<span class="font-medium">Model:</span> ${result.model || 'N/A'} |
<span class="font-medium">Time:</span> ${result.time}ms
</div>
</div>
`;
} else {
const modelBadgeClass = getModelBadgeClass(result.model);
const results = result.data.results || [];
resultCard.innerHTML = `
<div class="p-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mr-3">
<i class="fas fa-search text-indigo-600"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 flex-1">${result.query}</h3>
<span class="model-badge ${modelBadgeClass} px-3 py-1 rounded-full text-sm font-medium">
${result.model}
</span>
</div>
<div class="mb-4">
<h4 class="font-bold text-gray-700 mb-2">Summary</h4>
<p class="text-gray-600">${result.data.summary || 'No summary available'}</p>
</div>
<div class="mb-4">
<h4 class="font-bold text-gray-700 mb-2">Results (${results.length})</h4>
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
${results.map((res, index) => `
<div class="border-l-2 border-indigo-200 pl-3 py-2">
<div class="flex justify-between">
<a href="${res.url}" target="_blank" class="font-medium text-indigo-600 hover:underline">${res.title || `Result ${index + 1}`}</a>
<span class="text-xs text-gray-500">${res.date || 'N/A'}</span>
</div>
<p class="text-sm text-gray-600 mt-1">${res.snippet || 'No snippet available'}</p>
<div class="text-xs text-gray-500 mt-1">${res.url}</div>
</div>
`).join('')}
</div>
</div>
<div class="text-sm text-gray-500">
<span class="font-medium">Response Time:</span> ${result.time}ms
</div>
</div>
`;
}
resultsContainer.appendChild(resultCard);
}
// Get model badge class
function getModelBadgeClass(model) {
const modelClasses = {
'sonar-pro': 'bg-purple-100 text-purple-800',
'sonar-small': 'bg-blue-100 text-blue-800',
'sonar-medium': 'bg-green-100 text-green-800',
'sonar-large': 'bg-amber-100 text-amber-800'
};
return modelClasses[model] || 'bg-gray-100 text-gray-800';
}
// Update metrics
function updateMetrics(results, totalTime) {
const total = results.length;
const successful = results.filter(r => !r.error).length;
const avgTime = total > 0 ? Math.round(results.reduce((sum, r) => sum + r.time, 0) / total) : 0;
const successRate = total > 0 ? Math.round((successful / total) * 100) : 0;
totalQueriesEl.textContent = total;
avgResponseTimeEl.textContent = `${avgTime}ms`;
successRateEl.textContent = `${successRate}%`;
}
// Render response time chart
function renderResponseTimeChart() {
if (responseTimeChart) {
responseTimeChart.destroy();
}
const times = resultsData.map(r => r.time);
const labels = resultsData.map((r, i) => `Q${i+1}`);
responseTimeChart = new Chart(responseTimeChartCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Response Time (ms)',
data: times,
backgroundColor: 'rgba(99, 102, 241, 0.7)',
borderColor: 'rgba(99, 102, 241, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Milliseconds'
}
}
}
}
});
}
</script>
</body>
</html>