citelynq.websample/wwwroot/index.html

795 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CiteLynq Web Sample</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.card {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
input[type="text"],
input[type="password"],
select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="text"]:focus,
input[type="password"]:focus,
select:focus {
outline: none;
border-color: #667eea;
}
.search-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.search-input-row {
display: flex;
gap: 10px;
}
.search-input-row input {
flex: 1;
}
.filters-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.filter-label {
font-size: 11px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@media (max-width: 768px) {
.filters-row {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.filters-row {
grid-template-columns: 1fr;
}
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.status-box {
background: #f5f5f5;
border-left: 4px solid #667eea;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
.status-box.success {
background: #e8f5e9;
border-left-color: #4caf50;
}
.status-box.error {
background: #ffebee;
border-left-color: #f44336;
}
.results {
display: none;
}
.results.show {
display: block;
}
.result-item {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #667eea;
transition: all 0.2s;
}
.result-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-left-color: #764ba2;
}
.result-title {
font-size: 1.3em;
font-weight: 700;
color: #333;
margin-bottom: 10px;
}
.result-meta {
display: flex;
gap: 15px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.meta-tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
background: white;
border-radius: 20px;
font-size: 13px;
color: #666;
border: 1px solid #e0e0e0;
}
.meta-tag.source {
background: #667eea;
color: white;
border-color: #667eea;
}
.result-abstract {
color: #555;
line-height: 1.6;
margin-bottom: 15px;
}
.citation-box {
background: white;
border-left: 4px solid #667eea;
padding: 12px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
margin-top: 12px;
color: #333;
display: flex;
justify-content: space-between;
align-items: start;
gap: 10px;
}
.citation-text {
flex: 1;
}
.btn-copy {
padding: 6px 12px;
font-size: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.btn-copy:hover {
background: #5568d3;
}
.score-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: white;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
border: 1px solid #e0e0e0;
}
.score-badge.high {
background: #e8f5e9;
color: #2e7d32;
border-color: #81c784;
}
.score-badge.medium {
background: #fff3e0;
color: #e65100;
border-color: #ffb74d;
}
.score-badge.low {
background: #f5f5f5;
color: #666;
border-color: #e0e0e0;
}
.result-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 5px;
}
.result-link:hover {
text-decoration: underline;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-results {
text-align: center;
padding: 60px 20px;
color: #666;
}
.no-results-icon {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.3;
}
.stats-bar {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats-bar span {
font-size: 14px;
color: #666;
}
.stats-bar strong {
color: #333;
}
small {
color: #666;
display: block;
margin-top: 5px;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 CiteLynq Web Sample</h1>
<p>Search verified citations from authoritative sources</p>
</div>
<div class="card">
<div class="form-group">
<label for="apiKey">🔑 API Key</label>
<input type="password" id="apiKey" placeholder="cl_your-api-key-here">
<small>Get your API key from <a href="https://citelynq.com/app/api-keys" target="_blank" style="color: #667eea;">citelynq.com/app/api-keys</a></small>
</div>
<h3 style="margin-bottom: 15px; color: #333;">🔎 Search Citations</h3>
<div class="search-container">
<!-- Search Input Row -->
<div class="search-input-row">
<input type="text" id="query" placeholder="Enter your search query..." value="climate change">
<button class="btn btn-primary" id="searchBtn" onclick="performSearch()">
Search
</button>
</div>
<!-- Filters Row -->
<div class="filters-row">
<div class="filter-group">
<label class="filter-label">Search Type</label>
<select id="searchType" onchange="toggleDateFields()">
<option value="semantic">Semantic (AI)</option>
<option value="fulltext">Full-Text</option>
<option value="bydate">By Date</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Data Source</label>
<select id="sourceType">
<option value="">All Sources</option>
<option value="arxiv">ArXiv</option>
<option value="pubmed">PubMed</option>
<option value="wikipedia">Wikipedia</option>
<option value="federalregister">Federal Register</option>
<option value="govinfo">GovInfo</option>
<option value="fred">FRED</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Citation Format</label>
<select id="citationStyle">
<option value="mla">MLA</option>
<option value="apa">APA</option>
<option value="chicago">Chicago</option>
<option value="bibtex">BibTeX</option>
<option value="bluebook">Bluebook</option>
<option value="endnote">EndNote</option>
<option value="ris">RIS</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Min. Relevance</label>
<select id="minRelevance">
<option value="all">All Results</option>
<option value="medium">Medium+</option>
<option value="high">High Only</option>
</select>
</div>
<!-- Date fields (hidden by default) -->
<div class="filter-group" id="monthField" style="display: none;">
<label class="filter-label">Month</label>
<select id="searchMonth">
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</div>
<div class="filter-group" id="dayField" style="display: none;">
<label class="filter-label">Day</label>
<select id="searchDay">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
<option value="23">23</option>
<option value="24">24</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
<option value="28">28</option>
<option value="29">29</option>
<option value="30">30</option>
<option value="31">31</option>
</select>
</div>
</div>
</div>
<div id="statusBox"></div>
</div>
<div class="card results" id="resultsCard">
<h2 style="margin-bottom: 20px;">📊 Search Results</h2>
<div id="statsBar"></div>
<div id="resultsList"></div>
</div>
</div>
<script>
// Load API key from localStorage
const apiKeyInput = document.getElementById('apiKey');
const savedApiKey = localStorage.getItem('citelynq_api_key');
if (savedApiKey) {
apiKeyInput.value = savedApiKey;
}
// Save API key when changed
apiKeyInput.addEventListener('change', () => {
localStorage.setItem('citelynq_api_key', apiKeyInput.value);
});
// Allow Enter key to trigger search
document.getElementById('query').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
// Toggle date fields based on search type
function toggleDateFields() {
const searchType = document.getElementById('searchType').value;
const monthField = document.getElementById('monthField');
const dayField = document.getElementById('dayField');
if (searchType === 'bydate') {
monthField.style.display = 'block';
dayField.style.display = 'block';
} else {
monthField.style.display = 'none';
dayField.style.display = 'none';
}
}
async function performSearch() {
const apiKey = apiKeyInput.value.trim();
const query = document.getElementById('query').value.trim();
const searchType = document.getElementById('searchType').value;
const sourceType = document.getElementById('sourceType').value;
const minRelevance = document.getElementById('minRelevance').value;
const searchBtn = document.getElementById('searchBtn');
const resultsCard = document.getElementById('resultsCard');
if (!apiKey) {
showStatus('error', '❌ Please enter your API key');
return;
}
if (!query) {
showStatus('error', '❌ Please enter a search query');
return;
}
// Save API key
localStorage.setItem('citelynq_api_key', apiKey);
// Reset UI
searchBtn.disabled = true;
resultsCard.classList.remove('show');
showStatus('', '<div class="spinner"></div><p style="text-align: center; margin-top: 10px;">Searching...</p>');
try {
// Build URL based on search type
let url;
if (searchType === 'semantic') {
url = `/api/search/semantic?q=${encodeURIComponent(query)}&limit=20`;
// Add min_similarity filter for semantic search
if (minRelevance === 'high') {
url += '&min_similarity=0.70';
} else if (minRelevance === 'medium') {
url += '&min_similarity=0.40';
}
} else if (searchType === 'bydate') {
const month = document.getElementById('searchMonth').value;
const day = document.getElementById('searchDay').value;
url = `/api/search/bydate?q=${encodeURIComponent(query)}&month=${month}&day=${day}&limit=20`;
// Add min_rank filter for date search
if (minRelevance === 'high') {
url += '&min_rank=0.15';
} else if (minRelevance === 'medium') {
url += '&min_rank=0.08';
}
} else {
url = `/api/search?q=${encodeURIComponent(query)}&limit=20`;
// Add min_rank filter for full-text search
if (minRelevance === 'high') {
url += '&min_rank=0.15';
} else if (minRelevance === 'medium') {
url += '&min_rank=0.08';
}
}
if (sourceType) {
url += `&source=${encodeURIComponent(sourceType)}`;
}
// Call API endpoint
const response = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': apiKey
}
});
const data = await response.json();
if (!response.ok) {
console.error('API Error:', data);
const errorMsg = data.error || data.message || data.title || response.statusText;
const errorDetails = data.details || data.detail || '';
showStatus('error', `❌ Error: ${errorMsg}${errorDetails ? '<br><small>' + errorDetails + '</small>' : ''}`);
searchBtn.disabled = false;
return;
}
showStatus('success', '✅ Search completed successfully!');
displayResults(data, searchType);
searchBtn.disabled = false;
} catch (error) {
showStatus('error', `❌ Error: ${error.message}`);
searchBtn.disabled = false;
}
}
function displayResults(data, searchType) {
const resultsCard = document.getElementById('resultsCard');
const statsBar = document.getElementById('statsBar');
const resultsList = document.getElementById('resultsList');
// Extract results (already filtered and sorted by backend)
const results = data.results || [];
const totalCount = data.count || results.length;
if (results.length === 0) {
resultsCard.classList.add('show');
statsBar.innerHTML = '';
resultsList.innerHTML = `
<div class="no-results">
<div class="no-results-icon">📄</div>
<h3>No results found</h3>
<p>Try adjusting your search query or selecting a different source</p>
</div>
`;
return;
}
// Show stats
let statsHtml = `<span><strong>${results.length}</strong> results`;
if (data.tokens_used) {
statsHtml += ` (${data.tokens_used} tokens used)`;
}
statsHtml += `</span>`;
statsBar.innerHTML = statsHtml;
// Build results HTML (results are already sorted by relevance from backend)
resultsList.innerHTML = results.map(result => formatResult(result, searchType)).join('');
resultsCard.classList.add('show');
}
function formatResult(result, searchType) {
const citationStyle = document.getElementById('citationStyle').value;
const title = result.title || 'Untitled';
const snippet = result.snippet || result.chunk_text || '';
const source = result.source || 'Unknown';
const publishedAt = result.published_at || '';
const sourceUrl = result.source_url || '#';
// Generate citation
const citation = formatCitation(result, citationStyle);
// Get relevance score
let scoreHtml = '';
if (searchType === 'semantic' && result.similarity !== undefined) {
const similarity = result.similarity * 100;
const label = getRelevanceLabel(similarity, true);
scoreHtml = `<span class="score-badge ${label.className}">${similarity.toFixed(0)}% ${label.text}</span>`;
} else if (result.rank !== undefined) {
const rank = result.rank;
const label = getRelevanceLabel(rank, false);
scoreHtml = `<span class="score-badge ${label.className}">${rank.toFixed(2)} ${label.text}</span>`;
}
let html = '<div class="result-item">';
html += `<div class="result-title">${escapeHtml(title)}</div>`;
html += '<div class="result-meta">';
html += `<span class="meta-tag source">${escapeHtml(source)}</span>`;
if (publishedAt) {
html += `<span class="meta-tag">📅 ${formatDate(publishedAt)}</span>`;
}
if (scoreHtml) {
html += scoreHtml;
}
html += '</div>';
if (snippet) {
const truncated = snippet.length > 400 ? snippet.substring(0, 400) + '...' : snippet;
html += `<div class="result-abstract">${truncated}</div>`;
}
if (citation) {
html += `<div class="citation-box">`;
html += `<div class="citation-text">${escapeHtml(citation)}</div>`;
html += `<button class="btn-copy" onclick="copyCitation('${escapeHtml(citation).replace(/'/g, "\\'")}')">📋 Copy</button>`;
html += `</div>`;
}
html += `<div style="margin-top: 15px;">`;
if (sourceUrl !== '#') {
html += `<a href="${escapeHtml(sourceUrl)}" target="_blank" class="result-link">View Source →</a>`;
}
html += `</div>`;
html += '</div>';
return html;
}
function formatCitation(result, style) {
const title = result.title || 'Untitled';
const source = result.source || 'Unknown';
const date = result.published_at ? new Date(result.published_at).getFullYear() : 'n.d.';
const id = result.document_id || result.id || '';
const sourceName = source.charAt(0).toUpperCase() + source.slice(1).toLowerCase();
if (style === 'mla') {
return `"${title}." ${sourceName}, ${date}. ${id}`;
} else if (style === 'apa') {
return `(${date}). ${title}. ${sourceName}. ${id}`;
} else if (style === 'chicago') {
return `"${title}." ${sourceName} (${date}). ${id}`;
} else if (style === 'bluebook') {
return `${title}, ${sourceName} (${date}). ${id}`;
} else if (style === 'bibtex') {
const cleanId = id.replace(/[^a-zA-Z0-9]/g, '');
return `@article{${cleanId},\n title = {${title}},\n journal = {${sourceName}},\n year = {${date}},\n note = {${id}}\n}`;
} else if (style === 'endnote') {
return `%0 Journal Article\n%T ${title}\n%J ${sourceName}\n%D ${date}\n%U ${id}`;
} else if (style === 'ris') {
return `TY - JOUR\nTI - ${title}\nJO - ${sourceName}\nPY - ${date}\nUR - ${id}\nER -`;
}
return `"${title}." ${sourceName}, ${date}. ${id}`;
}
function getRelevanceLabel(score, isSimilarity) {
if (isSimilarity) {
if (score >= 70) return { text: 'High', className: 'high' };
if (score >= 40) return { text: 'Medium', className: 'medium' };
return { text: 'Low', className: 'low' };
} else {
if (score >= 0.15) return { text: 'High', className: 'high' };
if (score >= 0.08) return { text: 'Medium', className: 'medium' };
return { text: 'Low', className: 'low' };
}
}
function copyCitation(citation) {
navigator.clipboard.writeText(citation).then(() => {
showStatus('success', '✅ Citation copied to clipboard!');
setTimeout(() => showStatus('', ''), 2000);
}).catch(err => {
showStatus('error', '❌ Failed to copy citation');
});
}
function showStatus(type, message) {
const statusBox = document.getElementById('statusBox');
statusBox.className = `status-box ${type}`;
statusBox.innerHTML = message;
}
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch {
return dateString;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>