| <!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 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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| </div> |
| </div> |
| </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"> |
| |
| </div> |
| </section> |
| </div> |
|
|
| |
| <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> |
| |
| 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'); |
| |
| |
| let queryCounter = 0; |
| let resultsData = []; |
| let responseTimeChart = null; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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>'; |
| }); |
| |
| |
| addQueryBtn.addEventListener('click', addQueryItem); |
| |
| |
| clearQueriesBtn.addEventListener('click', clearQueries); |
| |
| |
| runQueriesBtn.addEventListener('click', runQueries); |
| |
| |
| addQueryItem(); |
| }); |
| |
| |
| 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); |
| |
| |
| queryItem.querySelector('.remove-query').addEventListener('click', (e) => { |
| const id = e.currentTarget.getAttribute('data-id'); |
| document.getElementById(id).remove(); |
| }); |
| } |
| |
| |
| function clearQueries() { |
| queriesContainer.innerHTML = ''; |
| queryCounter = 0; |
| addQueryItem(); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| loadingOverlay.classList.remove('hidden'); |
| progressText.textContent = `0/${queryItems.length} queries completed`; |
| |
| |
| resultsContainer.innerHTML = ''; |
| resultsData = []; |
| |
| |
| 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(); |
| |
| |
| 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`; |
| |
| |
| resultsData.push(...batchResults); |
| |
| |
| batchResults.forEach(result => { |
| addResultToDOM(result); |
| }); |
| } |
| |
| |
| loadingOverlay.classList.add('hidden'); |
| |
| |
| resultsSection.classList.remove('hidden'); |
| |
| |
| updateMetrics(resultsData, Date.now() - startTime); |
| |
| |
| renderResponseTimeChart(); |
| } |
| |
| 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 { |
| |
| const requestData = { |
| query, |
| model, |
| search_domain_filter: domains, |
| date_filter: `${recency}d`, |
| max_tokens_per_page: tokens |
| }; |
| |
| if (country) { |
| requestData.country_filter = country; |
| } |
| |
| |
| 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 |
| }; |
| } |
| } |
| |
| 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); |
| } |
| |
| |
| 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'; |
| } |
| |
| |
| 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}%`; |
| } |
| |
| |
| 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> |
|
|