AIFinder / templates /index.html
CompactAI's picture
Upload 18 files
bb0efe6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AIFinder - Identify AI Responses</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0d0d0d;
--bg-secondary: #171717;
--bg-tertiary: #1f1f1f;
--bg-elevated: #262626;
--text-primary: #f5f5f5;
--text-secondary: #a3a3a3;
--text-muted: #737373;
--accent: #e85d04;
--accent-hover: #f48c06;
--accent-muted: #9c4300;
--success: #22c55e;
--success-muted: #166534;
--border: #333333;
--border-light: #404040;
}
body {
font-family: 'Outfit', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
header {
text-align: center;
margin-bottom: 3rem;
padding-top: 1rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.05em;
margin-bottom: 0.5rem;
}
.logo span {
color: var(--accent);
}
.tagline {
color: var(--text-secondary);
font-size: 1rem;
font-weight: 300;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: border-color 0.2s ease;
}
.card:focus-within {
border-color: var(--border-light);
}
.card-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-bottom: 0.75rem;
font-weight: 500;
}
textarea {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
resize: vertical;
min-height: 180px;
transition: border-color 0.2s ease;
}
textarea:focus {
outline: none;
border-color: var(--accent-muted);
}
textarea::placeholder {
color: var(--text-muted);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-elevated);
border-color: var(--border-light);
}
.btn-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.results {
display: none;
}
.results.visible {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem;
background: var(--bg-tertiary);
border-radius: 8px;
margin-bottom: 1rem;
}
.result-provider {
font-size: 1.5rem;
font-weight: 600;
}
.result-confidence {
font-size: 1.25rem;
font-weight: 500;
color: var(--accent);
}
.result-bar {
height: 8px;
background: var(--bg-elevated);
border-radius: 4px;
margin-bottom: 1rem;
overflow: hidden;
}
.result-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 4px;
transition: width 0.5s ease;
}
.result-list {
list-style: none;
}
.result-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.result-item:last-child {
border-bottom: none;
}
.result-name {
font-weight: 500;
}
.result-percent {
font-family: 'JetBrains Mono', monospace;
color: var(--text-secondary);
font-size: 0.875rem;
}
.correction {
display: none;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.correction.visible {
display: block;
animation: fadeIn 0.3s ease;
}
.correction-title {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.75rem;
color: var(--text-secondary);
}
select {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
margin-bottom: 0.75rem;
cursor: pointer;
}
select:focus {
outline: none;
border-color: var(--accent-muted);
}
.stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.stat {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.25rem;
flex: 1;
min-width: 120px;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--accent);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.5rem;
color: var(--text-primary);
font-size: 0.9rem;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 1000;
}
.toast.visible {
opacity: 1;
transform: translateY(0);
}
.toast.success {
border-color: var(--success-muted);
}
.footer {
text-align: center;
margin-top: 3rem;
padding: 1.5rem;
color: var(--text-muted);
font-size: 0.8rem;
}
.footer a {
color: var(--text-secondary);
text-decoration: none;
}
.footer a:hover {
color: var(--accent);
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--text-muted);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
}
.status-dot.loading {
background: var(--accent);
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* ── Tabs ── */
.tabs {
display: flex;
gap: 0;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border);
}
.tab {
padding: 0.75rem 1.5rem;
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-muted);
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.tab:hover {
color: var(--text-secondary);
}
.tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
/* ── API Docs ── */
.docs-section {
margin-bottom: 2rem;
}
.docs-section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.docs-section h3 {
font-size: 1rem;
font-weight: 500;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.docs-section p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.75rem;
line-height: 1.7;
}
.docs-endpoint {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
}
.docs-method {
color: var(--success);
font-weight: 600;
}
.docs-path {
color: var(--text-primary);
}
.docs-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.2rem 0.6rem;
border-radius: 4px;
margin-left: 0.5rem;
}
.docs-badge.free {
background: var(--success-muted);
color: var(--success);
}
.docs-badge.limit {
background: var(--accent-muted);
color: var(--accent-hover);
}
.docs-code-block {
position: relative;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.docs-code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border);
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.docs-copy-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
font-family: 'Outfit', sans-serif;
transition: all 0.2s ease;
}
.docs-copy-btn:hover {
color: var(--text-primary);
border-color: var(--border-light);
}
.docs-code-block pre {
padding: 1rem;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
line-height: 1.6;
color: var(--text-primary);
margin: 0;
}
.docs-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.docs-table th {
text-align: left;
padding: 0.6rem 0.75rem;
background: var(--bg-elevated);
color: var(--text-secondary);
font-weight: 500;
border-bottom: 1px solid var(--border);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.docs-table td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
}
.docs-table tr:last-child td {
border-bottom: none;
}
.docs-table code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
background: var(--bg-tertiary);
padding: 0.15rem 0.4rem;
border-radius: 3px;
color: var(--accent-hover);
}
.docs-warning {
background: rgba(232, 93, 4, 0.08);
border: 1px solid var(--accent-muted);
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.7;
}
.docs-warning strong {
color: var(--accent-hover);
}
.docs-inline-code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
background: var(--bg-tertiary);
padding: 0.15rem 0.4rem;
border-radius: 3px;
color: var(--accent-hover);
}
.docs-try-it {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.25rem;
margin-top: 1rem;
}
.docs-try-it textarea {
min-height: 100px;
margin-bottom: 0.75rem;
}
.docs-try-output {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
display: none;
}
.docs-try-output.visible {
display: block;
animation: fadeIn 0.3s ease;
}
.format-option:hover {
border-color: var(--border-light) !important;
background: var(--bg-elevated) !important;
}
.format-option:has(input:checked) {
border-color: var(--accent-muted) !important;
background: rgba(232, 93, 4, 0.08) !important;
}
@media (max-width: 600px) {
.container {
padding: 1rem;
}
.logo {
font-size: 2rem;
}
.btn-group {
flex-direction: column;
}
.btn {
width: 100%;
}
.result-main {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">AI<span>Finder</span></div>
<p class="tagline">Identify which AI provider generated a response</p>
</header>
<div class="tabs">
<button class="tab active" data-tab="classify">Classify</button>
<button class="tab" data-tab="dataset">Evaluate Dataset</button>
<button class="tab" data-tab="docs">API Docs</button>
</div>
<!-- ═══ Classify Tab ═══ -->
<div class="tab-content active" id="tab-classify">
<div class="status-indicator">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Connecting to API...</span>
<span id="providerCount" style="margin-left:auto;font-size:0.75rem;color:var(--text-muted);"></span>
</div>
<div class="card">
<div class="card-label">Paste AI Response</div>
<textarea id="inputText" placeholder="Paste an AI response here to identify which provider generated it..."></textarea>
</div>
<div class="btn-group">
<button class="btn btn-primary" id="classifyBtn" disabled>
<span id="classifyBtnText">Classify</span>
</button>
<button class="btn btn-secondary" id="clearBtn">Clear</button>
</div>
<div class="results" id="results">
<div class="card">
<div class="card-label">Result</div>
<div class="result-main">
<span class="result-provider" id="resultProvider">-</span>
<span class="result-confidence" id="resultConfidence">-</span>
</div>
<div class="result-bar">
<div class="result-bar-fill" id="resultBar" style="width: 0%"></div>
</div>
<ul class="result-list" id="resultList"></ul>
</div>
<div class="correction" id="correction">
<div class="correction-title">Wrong? Correct the provider to train the model:</div>
<select id="providerSelect"></select>
<button class="btn btn-primary" id="trainBtn">Train & Save</button>
</div>
</div>
<div class="stats" id="stats" style="display: none;">
<div class="stat">
<div class="stat-value" id="correctionsCount">0</div>
<div class="stat-label">Corrections</div>
</div>
<div class="stat">
<div class="stat-value" id="sessionCount">0</div>
<div class="stat-label">Session</div>
</div>
</div>
<div class="actions" id="actions" style="display: none;">
<button class="btn btn-secondary" id="exportBtn">Export Trained Model</button>
<button class="btn btn-secondary" id="communityBtn" style="display:none;">Use Community Model</button>
<button class="btn btn-secondary" id="resetBtn">Reset Training</button>
</div>
<div id="communityWarning" style="display:none; margin-top:1rem; background:rgba(232,93,4,0.12); border:1px solid var(--accent-muted); border-radius:8px; padding:1rem 1.25rem; font-size:0.85rem; color:var(--text-secondary); line-height:1.7;">
⚠️ <strong style="color:var(--accent-hover);">Community Model Active</strong> — This is a community-trained version. It could be <strong style="color:var(--accent-hover);">VERY wrong</strong>. Results may be unreliable. Use at your own risk.
</div>
</div>
<!-- ═══ Dataset Evaluation Tab ═══ -->
<div class="tab-content" id="tab-dataset">
<div class="card">
<div class="card-label">HuggingFace Dataset ID</div>
<input type="text" id="datasetId" placeholder="e.g., ianncity/Hunter-Alpha-SFT-300000x"
style="width:100%; padding:0.75rem 1rem; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:8px; color:var(--text-primary); font-family:'Outfit',sans-serif;font-size:0.9rem;margin-bottom:0.75rem;">
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
<button class="btn btn-secondary" id="checkDatasetBtn">Check Format</button>
<button class="btn btn-primary" id="evaluateDatasetBtn" disabled>Evaluate</button>
<input type="number" id="maxSamples" value="1000" min="1" max="10000"
style="width:100px;padding:0.5rem;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);font-size:0.85rem;">
<span style="color:var(--text-muted);font-size:0.8rem;">max samples</span>
</div>
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;">
<div class="card-label" style="margin-bottom:0;">Dataset Format</div>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;">
<input type="checkbox" id="useCustomFormat" style="width:16px;height:16px;accent-color:var(--accent);">
<span style="font-size:0.8rem;color:var(--text-secondary);">Use custom format</span>
</label>
</div>
<div id="customFormatSection" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:1rem;">
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:0.75rem;">
How is your dataset structured? Choose a format below:
</div>
<div style="display:grid;gap:0.5rem;margin-bottom:1rem;">
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;padding:0.5rem;background:var(--bg-secondary);border-radius:6px;border:1px solid transparent;" class="format-option" data-format="auto">
<input type="radio" name="customFormatType" value="auto" checked style="accent-color:var(--accent);">
<div>
<div style="font-weight:500;font-size:0.85rem;">Auto-detect</div>
<div style="font-size:0.75rem;color:var(--text-muted);">Try to detect format automatically</div>
</div>
</label>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;padding:0.5rem;background:var(--bg-secondary);border-radius:6px;border:1px solid transparent;" class="format-option" data-format="column">
<input type="radio" name="customFormatType" value="column" style="accent-color:var(--accent);">
<div>
<div style="font-weight:500;font-size:0.85rem;">Single column</div>
<div style="font-size:0.75rem;color:var(--text-muted);">Extract from one field (e.g., "response")</div>
</div>
</label>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;padding:0.5rem;background:var(--bg-secondary);border-radius:6px;border:1px solid transparent;" class="format-option" data-format="two_column">
<input type="radio" name="customFormatType" value="two_column" style="accent-color:var(--accent);">
<div>
<div style="font-weight:500;font-size:0.85rem;">Two columns</div>
<div style="font-size:0.75rem;color:var(--text-muted);">User column + Assistant column</div>
</div>
</label>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;padding:0.5rem;background:var(--bg-secondary);border-radius:6px;border:1px solid transparent;" class="format-option" data-format="pattern">
<input type="radio" name="customFormatType" value="pattern" style="accent-color:var(--accent);">
<div>
<div style="font-weight:500;font-size:0.85rem;">Text markers</div>
<div style="font-size:0.75rem;color:var(--text-muted);">Extract between text markers</div>
</div>
</label>
</div>
<div id="columnInput" style="display:none;">
<input type="text" id="customColumnName" placeholder="e.g., response, output, completion"
style="width:100%; padding:0.6rem 0.75rem; background:var(--bg-primary); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-family:'JetBrains Mono',monospace;font-size:0.85rem;">
</div>
<div id="twoColumnInput" style="display:none;">
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<input type="text" id="customUserColumn" placeholder="User column (e.g., prompt, input)"
style="flex:1;min-width:150px;padding:0.6rem 0.75rem; background:var(--bg-primary); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-family:'JetBrains Mono',monospace;font-size:0.85rem;">
<input type="text" id="customAssistantColumn" placeholder="Assistant column (e.g., response, output)"
style="flex:1;min-width:150px;padding:0.6rem 0.75rem; background:var(--bg-primary); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-family:'JetBrains Mono',monospace;font-size:0.85rem;">
</div>
</div>
<div id="patternInput" style="display:none;">
<input type="text" id="customPattern" placeholder="e.g., user:[INST] assistant:[/INST] or [startuser] [startassistant]"
style="width:100%; padding:0.6rem 0.75rem; background:var(--bg-primary); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-family:'JetBrains Mono',monospace;font-size:0.85rem;">
<div style="font-size:0.7rem;color:var(--text-muted);margin-top:0.5rem;">
Use <code style="background:var(--bg-primary);padding:0.1rem 0.3rem;border-radius:3px;">[startuser]</code> and <code style="background:var(--bg-primary);padding:0.1rem 0.3rem;border-radius:3px;">[startassistant]</code> as placeholders, or raw text like <code style="background:var(--bg-primary);padding:0.1rem 0.3rem;border-radius:3px;">user: assistant:</code>
</div>
</div>
<div style="margin-top:0.75rem;padding:0.5rem;background:var(--bg-primary);border-radius:6px;">
<div style="font-size:0.7rem;color:var(--text-muted);margin-bottom:0.25rem;">Format string preview:</div>
<code id="formatPreview" style="font-family:'JetBrains Mono',monospace;font-size:0.8rem;color:var(--accent);">column: response</code>
</div>
</div>
</div>
</div>
<div id="datasetFormatInfo" class="card" style="display:none;">
<div class="card-label">Dataset Format</div>
<div id="formatName" style="font-weight:600;margin-bottom:0.5rem;"></div>
<div id="formatDescription" style="color:var(--text-secondary);font-size:0.9rem;"></div>
<div style="margin-top:0.75rem;display:flex;gap:1rem;">
<div class="stat" style="padding:0.5rem 1rem;min-width:auto;">
<div class="stat-value" id="totalRows" style="font-size:1rem;">-</div>
<div class="stat-label" style="font-size:0.65rem;">Total Rows</div>
</div>
<div class="stat" style="padding:0.5rem 1rem;min-width:auto;">
<div class="stat-value" id="extractedCount" style="font-size:1rem;">-</div>
<div class="stat-label" style="font-size:0.65rem;">Responses</div>
</div>
</div>
<div id="formatError" style="display:none;margin-top:1rem;padding:0.75rem;background:rgba(232,93,4,0.12);border:1px solid var(--accent-muted);border-radius:8px;color:var(--text-secondary);font-size:0.85rem;"></div>
</div>
<div id="datasetResults" class="card" style="display:none;">
<div class="card-label">Evaluation Results</div>
<div style="display:flex;gap:1rem;margin-bottom:1.5rem;flex-wrap:wrap;">
<div class="stat">
<div class="stat-value" id="evalTotal">-</div>
<div class="stat-label">Samples</div>
</div>
<div class="stat">
<div class="stat-value" id="evalLikelyProvider">-</div>
<div class="stat-label">Likely Provider</div>
</div>
<div class="stat">
<div class="stat-value" id="evalAvgConfidence">-</div>
<div class="stat-label">Avg Confidence</div>
</div>
</div>
<div class="card-label" style="margin-top:1rem;">Provider Distribution</div>
<div id="providerDistribution"></div>
<div class="card-label" style="margin-top:1.5rem;">Top Providers (by cumulative score)</div>
<div id="topProvidersList"></div>
</div>
<div id="datasetLoading" style="display:none;text-align:center;padding:2rem;">
<span class="loading" style="width:24px;height:24px;border-width:3px;"></span>
<div style="margin-top:1rem;color:var(--text-secondary);" id="datasetLoadingText">Evaluating...</div>
</div>
<div class="docs-section" style="margin-top:2rem;">
<h2 style="font-size:1rem;font-weight:500;color:var(--text-secondary);margin-bottom:0.75rem;">Supported Dataset Formats</h2>
<div id="supportedFormatsList" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:0.75rem;"></div>
</div>
<div class="card" style="margin-top:2rem;">
<div class="card-label" style="display:flex;justify-content:space-between;align-items:center;">
<span>Your Evaluated Datasets</span>
<button class="btn btn-secondary" id="clearHistoryBtn" style="padding:0.4rem 0.75rem;font-size:0.75rem;">Clear History</button>
</div>
<div id="datasetHistory" style="color:var(--text-muted);font-size:0.85rem;">Loading...</div>
</div>
</div>
<!-- ═══ API Docs Tab ═══ -->
<div class="tab-content" id="tab-docs">
<div class="docs-section">
<h2>Public Classification API</h2>
<p>
AIFinder exposes a free, public endpoint for programmatic classification.
No API key required.
</p>
<div>
<div class="docs-endpoint">
<span class="docs-method">POST</span>
<span class="docs-path">/v1/classify</span>
</div>
<span class="docs-badge free">No API Key</span>
<span class="docs-badge limit">60 req/min</span>
</div>
</div>
<!-- ── Request ── -->
<div class="docs-section">
<h2>Request</h2>
<p>Send a JSON body with <span class="docs-inline-code">Content-Type: application/json</span>.</p>
<table class="docs-table">
<thead>
<tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead>
<tbody>
<tr>
<td><code>text</code></td>
<td>string</td>
<td>Yes</td>
<td>The AI-generated text to classify (min 20 chars)</td>
</tr>
<tr>
<td><code>top_n</code></td>
<td>integer</td>
<td>No</td>
<td>Number of results to return (default: <strong>5</strong>)</td>
</tr>
</tbody>
</table>
<div class="docs-warning">
<strong>⚠️ Strip thought tags!</strong><br>
Many reasoning models wrap chain-of-thought in
<span class="docs-inline-code">&lt;think&gt;&lt;/think&gt;</span> or
<span class="docs-inline-code">&lt;thinking&gt;&lt;/thinking&gt;</span> blocks.
These confuse the classifier. The API strips them automatically, but you should
remove them on your side too to save bandwidth.
</div>
</div>
<!-- ── Response ── -->
<div class="docs-section">
<h2>Response</h2>
<div class="docs-code-block">
<div class="docs-code-header">
<span>JSON</span>
<button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>{
"provider": "Anthropic",
"confidence": 87.42,
"top_providers": [
{ "name": "Anthropic", "confidence": 87.42 },
{ "name": "OpenAI", "confidence": 6.15 },
{ "name": "Google", "confidence": 3.28 },
{ "name": "xAI", "confidence": 1.74 },
{ "name": "DeepSeek", "confidence": 0.89 }
]
}</pre>
</div>
<table class="docs-table">
<thead>
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
<tr>
<td><code>provider</code></td>
<td>string</td>
<td>Best-matching provider name</td>
</tr>
<tr>
<td><code>confidence</code></td>
<td>float</td>
<td>Confidence % for the top provider</td>
</tr>
<tr>
<td><code>top_providers</code></td>
<td>array</td>
<td>Ranked list of <code>{ name, confidence }</code> objects</td>
</tr>
</tbody>
</table>
</div>
<!-- ── Errors ── -->
<div class="docs-section">
<h2>Errors</h2>
<table class="docs-table">
<thead>
<tr><th>Status</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td><code>400</code></td><td>Missing <code>text</code> field or text shorter than 20 characters</td></tr>
<tr><td><code>429</code></td><td>Rate limit exceeded (60 requests/minute per IP)</td></tr>
</tbody>
</table>
</div>
<!-- ── Code Examples ── -->
<div class="docs-section">
<h2>Code Examples</h2>
<h3>cURL</h3>
<div class="docs-code-block">
<div class="docs-code-header">
<span>Bash</span>
<button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>curl -X POST https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify \
-H "Content-Type: application/json" \
-d '{
"text": "I would be happy to help you with that! Here is a detailed explanation of how neural networks work...",
"top_n": 5
}'</pre>
</div>
<h3>Python</h3>
<div class="docs-code-block">
<div class="docs-code-header">
<span>Python</span>
<button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>import re
import requests
API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify"
def strip_think_tags(text):
"""Remove &lt;think&gt;/&lt;thinking&gt; blocks before classifying."""
return re.sub(r"&lt;think(?:ing)?&gt;.*?&lt;/think(?:ing)?&gt;",
"", text, flags=re.DOTALL).strip()
text = """I'd be happy to help! Neural networks are
computational models inspired by the human brain..."""
# Strip thought tags first (the API does this too,
# but saves bandwidth to do it client-side)
cleaned = strip_think_tags(text)
response = requests.post(API_URL, json={
"text": cleaned,
"top_n": 5
})
data = response.json()
print(f"Provider: {data['provider']} ({data['confidence']:.1f}%)")
for p in data["top_providers"]:
print(f" {p['name']:&lt;20s} {p['confidence']:5.1f}%")</pre>
</div>
<h3>JavaScript (fetch)</h3>
<div class="docs-code-block">
<div class="docs-code-header">
<span>JavaScript</span>
<button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>const API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify";
function stripThinkTags(text) {
return text.replace(/&lt;think(?:ing)?&gt;[\s\S]*?&lt;\/think(?:ing)?&gt;/g, "").trim();
}
async function classify(text, topN = 5) {
const cleaned = stripThinkTags(text);
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: cleaned, top_n: topN })
});
return res.json();
}
// Usage
classify("I'd be happy to help you understand...")
.then(data =&gt; {
console.log(`Provider: ${data.provider} (${data.confidence}%)`);
data.top_providers.forEach(p =&gt;
console.log(` ${p.name}: ${p.confidence}%`)
);
});</pre>
</div>
<h3>Node.js</h3>
<div class="docs-code-block">
<div class="docs-code-header">
<span>JavaScript (Node)</span>
<button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>const API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify";
async function classify(text, topN = 5) {
const cleaned = text
.replace(/&lt;think(?:ing)?&gt;[\s\S]*?&lt;\/think(?:ing)?&gt;/g, "")
.trim();
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: cleaned, top_n: topN })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
// Example
(async () =&gt; {
const result = await classify(
"Let me think about this step by step...",
3
);
console.log(result);
})();</pre>
</div>
</div>
<!-- ── Try It ── -->
<div class="docs-section">
<h2>Try It</h2>
<p>Test the API right here — paste any AI-generated text and hit Send.</p>
<div class="docs-try-it">
<textarea id="docsTestInput" placeholder="Paste AI-generated text here..."></textarea>
<div class="btn-group">
<button class="btn btn-primary" id="docsTestBtn">Send Request</button>
</div>
<div class="docs-try-output" id="docsTestOutput"></div>
</div>
</div>
<!-- ── Providers ── -->
<div class="docs-section">
<h2>Supported Providers</h2>
<p>The classifier currently supports these providers:</p>
<div id="docsProviderList" style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;"></div>
</div>
</div>
<div class="footer">
<p>AIFinder &mdash; Train on corrections to improve accuracy</p>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:7860'
: '';
let providers = [];
let correctionsCount = 0;
let sessionCorrections = 0;
const inputText = document.getElementById('inputText');
const classifyBtn = document.getElementById('classifyBtn');
const classifyBtnText = document.getElementById('classifyBtnText');
const clearBtn = document.getElementById('clearBtn');
const results = document.getElementById('results');
const resultProvider = document.getElementById('resultProvider');
const resultConfidence = document.getElementById('resultConfidence');
const resultBar = document.getElementById('resultBar');
const resultList = document.getElementById('resultList');
const correction = document.getElementById('correction');
const providerSelect = document.getElementById('providerSelect');
const trainBtn = document.getElementById('trainBtn');
const stats = document.getElementById('stats');
const correctionsCountEl = document.getElementById('correctionsCount');
const sessionCountEl = document.getElementById('sessionCount');
const actions = document.getElementById('actions');
const exportBtn = document.getElementById('exportBtn');
const communityBtn = document.getElementById('communityBtn');
const communityWarning = document.getElementById('communityWarning');
const resetBtn = document.getElementById('resetBtn');
const toast = document.getElementById('toast');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const providerCountEl = document.getElementById('providerCount');
let usingCommunity = false;
function showToast(message, type = 'info') {
toast.textContent = message;
toast.className = 'toast visible' + (type === 'success' ? ' success' : '');
setTimeout(() => {
toast.classList.remove('visible');
}, 3000);
}
async function checkStatus() {
try {
const res = await fetch(`${API_BASE}/api/status`);
const data = await res.json();
if (data.loaded) {
statusDot.classList.remove('loading');
statusText.textContent = data.using_community ? 'Ready — Community Model (cpu)' : `Ready (${data.device})`;
if (data.num_providers) {
providerCountEl.textContent = `${data.num_providers} providers`;
}
classifyBtn.disabled = false;
usingCommunity = data.using_community;
updateCommunityUI(data.community_available);
if (data.corrections_count > 0) {
correctionsCount = data.corrections_count;
correctionsCountEl.textContent = correctionsCount;
stats.style.display = 'flex';
actions.style.display = 'flex';
}
loadProviders();
loadStats();
} else {
setTimeout(checkStatus, 1000);
}
} catch (e) {
statusDot.classList.add('loading');
statusText.textContent = 'Connecting to API...';
setTimeout(checkStatus, 2000);
}
}
function updateCommunityUI(available) {
if (available) {
communityBtn.style.display = '';
communityBtn.textContent = usingCommunity ? 'Use Official Model' : 'Use Community Model';
communityWarning.style.display = usingCommunity ? 'block' : 'none';
actions.style.display = 'flex';
} else {
communityBtn.style.display = 'none';
communityWarning.style.display = 'none';
}
}
async function loadProviders() {
const res = await fetch(`${API_BASE}/api/providers`);
const data = await res.json();
providers = data.providers;
providerSelect.innerHTML = providers.map(p =>
`<option value="${p}">${p}</option>`
).join('');
}
function loadStats() {
const saved = localStorage.getItem('aifinder_corrections');
if (saved) {
correctionsCount = parseInt(saved, 10);
correctionsCountEl.textContent = correctionsCount;
stats.style.display = 'flex';
actions.style.display = 'flex';
}
sessionCountEl.textContent = sessionCorrections;
}
function saveStats() {
localStorage.setItem('aifinder_corrections', correctionsCount.toString());
}
async function classify() {
const text = inputText.value.trim();
if (text.length < 20) {
showToast('Text must be at least 20 characters');
return;
}
classifyBtn.disabled = true;
classifyBtnText.innerHTML = '<span class="loading"></span>';
try {
const res = await fetch(`${API_BASE}/api/classify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!res.ok) {
throw new Error('Classification failed');
}
const data = await res.json();
showResults(data);
} catch (e) {
showToast('Error: ' + e.message);
} finally {
classifyBtn.disabled = false;
classifyBtnText.textContent = 'Classify';
}
}
function showResults(data) {
resultProvider.textContent = data.provider;
resultConfidence.textContent = data.confidence.toFixed(1) + '%';
resultBar.style.width = data.confidence + '%';
resultList.innerHTML = data.top_providers.map(p => `
<li class="result-item">
<span class="result-name">${p.name}</span>
<span class="result-percent">${p.confidence.toFixed(1)}%</span>
</li>
`).join('');
providerSelect.value = data.provider;
results.classList.add('visible');
correction.classList.add('visible');
if (correctionsCount > 0 || sessionCorrections > 0) {
stats.style.display = 'flex';
actions.style.display = 'flex';
}
}
async function train() {
const text = inputText.value.trim();
const correctProvider = providerSelect.value;
trainBtn.disabled = true;
trainBtn.innerHTML = '<span class="loading"></span>';
try {
const res = await fetch(`${API_BASE}/api/correct`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, correct_provider: correctProvider })
});
if (!res.ok) {
throw new Error('Training failed');
}
const data = await res.json();
correctionsCount = data.corrections || correctionsCount + 1;
sessionCorrections++;
saveStats();
correctionsCountEl.textContent = correctionsCount;
sessionCountEl.textContent = sessionCorrections;
showToast('Correction saved & community model retrained!', 'success');
stats.style.display = 'flex';
actions.style.display = 'flex';
updateCommunityUI(true);
classify();
} catch (e) {
showToast('Error: ' + e.message);
} finally {
trainBtn.disabled = false;
trainBtn.textContent = 'Train & Save';
}
}
async function exportModel() {
exportBtn.disabled = true;
exportBtn.innerHTML = '<span class="loading"></span>';
try {
const res = await fetch(`${API_BASE}/api/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: 'aifinder_trained.pt' })
});
if (!res.ok) {
throw new Error('Save failed');
}
const data = await res.json();
const link = document.createElement('a');
link.href = `${API_BASE}/models/${data.filename}`;
link.download = data.filename;
link.click();
showToast('Model exported!', 'success');
} catch (e) {
showToast('Error: ' + e.message);
} finally {
exportBtn.disabled = false;
exportBtn.textContent = 'Export Trained Model';
}
}
function resetTraining() {
if (!confirm('Reset all training data? This cannot be undone.')) {
return;
}
correctionsCount = 0;
sessionCorrections = 0;
localStorage.removeItem('aifinder_corrections');
correctionsCountEl.textContent = '0';
sessionCountEl.textContent = '0';
stats.style.display = 'none';
actions.style.display = 'none';
showToast('Training data reset');
}
classifyBtn.addEventListener('click', classify);
clearBtn.addEventListener('click', () => {
inputText.value = '';
results.classList.remove('visible');
correction.classList.remove('visible');
});
trainBtn.addEventListener('click', train);
exportBtn.addEventListener('click', exportModel);
resetBtn.addEventListener('click', resetTraining);
communityBtn.addEventListener('click', async () => {
communityBtn.disabled = true;
try {
const res = await fetch(`${API_BASE}/api/toggle_community`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !usingCommunity })
});
const data = await res.json();
usingCommunity = data.using_community;
updateCommunityUI(data.available);
statusText.textContent = usingCommunity ? 'Ready — Community Model (cpu)' : 'Ready (cpu)';
showToast(usingCommunity ? 'Switched to community model' : 'Switched to official model', 'success');
} catch (e) {
showToast('Error: ' + e.message);
} finally {
communityBtn.disabled = false;
}
});
inputText.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
classify();
}
});
// ── Tab switching ──
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
// ── Copy button for code blocks ──
function copyCode(btn) {
const pre = btn.closest('.docs-code-block').querySelector('pre');
navigator.clipboard.writeText(pre.textContent).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
});
}
// ── Docs: populate provider badges ──
function populateDocsProviders() {
const list = document.getElementById('docsProviderList');
if (!list || !providers.length) return;
list.innerHTML = providers.map(p =>
`<span class="docs-inline-code" style="padding:0.3rem 0.75rem;">${p}</span>`
).join('');
}
// ── Docs: "Try It" live tester ──
const docsTestBtn = document.getElementById('docsTestBtn');
const docsTestInput = document.getElementById('docsTestInput');
const docsTestOutput = document.getElementById('docsTestOutput');
if (docsTestBtn) {
docsTestBtn.addEventListener('click', async () => {
const text = docsTestInput.value.trim();
if (text.length < 20) {
docsTestOutput.textContent = '{"error": "Text too short (minimum 20 characters)"}';
docsTestOutput.classList.add('visible');
return;
}
docsTestBtn.disabled = true;
docsTestBtn.innerHTML = '<span class="loading"></span>';
try {
const res = await fetch(`${API_BASE}/v1/classify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, top_n: 5 })
});
const data = await res.json();
docsTestOutput.textContent = JSON.stringify(data, null, 2);
} catch (e) {
docsTestOutput.textContent = `{"error": "${e.message}"}`;
}
docsTestOutput.classList.add('visible');
docsTestBtn.disabled = false;
docsTestBtn.textContent = 'Send Request';
});
}
// Hook provider list population into the existing load flow
const _origLoadProviders = loadProviders;
loadProviders = async function() {
await _origLoadProviders();
populateDocsProviders();
};
// ── Dataset Evaluation ──
const datasetIdInput = document.getElementById('datasetId');
const maxSamplesInput = document.getElementById('maxSamples');
const checkDatasetBtn = document.getElementById('checkDatasetBtn');
const evaluateDatasetBtn = document.getElementById('evaluateDatasetBtn');
const datasetFormatInfo = document.getElementById('datasetFormatInfo');
const formatName = document.getElementById('formatName');
const formatDescription = document.getElementById('formatDescription');
const totalRowsEl = document.getElementById('totalRows');
const extractedCountEl = document.getElementById('extractedCount');
const formatError = document.getElementById('formatError');
const datasetResults = document.getElementById('datasetResults');
const datasetLoading = document.getElementById('datasetLoading');
const datasetLoadingText = document.getElementById('datasetLoadingText');
const datasetHistory = document.getElementById('datasetHistory');
let currentDatasetInfo = null;
let currentJobId = null;
let jobPollingInterval = null;
function saveJobId(jobId) {
localStorage.setItem('aifinder_current_job', jobId);
}
function getSavedJobId() {
return localStorage.getItem('aifinder_current_job');
}
function clearSavedJobId() {
localStorage.removeItem('aifinder_current_job');
}
function generateApiKey() {
const existing = localStorage.getItem('aifinder_api_key');
if (existing) return existing;
const key = 'usr_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
localStorage.setItem('aifinder_api_key', key);
return key;
}
function getApiKey() {
return localStorage.getItem('aifinder_api_key') || generateApiKey();
}
getApiKey();
async function loadDatasetHistory() {
const apiKey = getApiKey();
if (!apiKey) {
datasetHistory.innerHTML = '<span style="color:var(--text-muted);">No evaluated datasets yet.</span>';
return;
}
try {
const res = await fetch(`${API_BASE}/api/datasets?api_key=${encodeURIComponent(apiKey)}`);
const data = await res.json();
if (!data.datasets || data.datasets.length === 0) {
datasetHistory.innerHTML = '<span style="color:var(--text-muted);">Your evaluated datasets will appear here. Start by checking a dataset format above.</span>';
return;
}
datasetHistory.innerHTML = data.datasets.map(ds => `
<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;margin-bottom:0.5rem;cursor:pointer;"
onclick="loadDatasetResult('${ds.job_id}')">
<div>
<div style="font-weight:500;">${ds.dataset_id}</div>
<div style="font-size:0.75rem;color:var(--text-muted);">${ds.completed_at ? new Date(ds.completed_at).toLocaleString() : ''}</div>
</div>
<span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;${ds.status === 'completed' ? 'background:var(--success-muted);color:var(--success);' : 'background:var(--accent-muted);color:var(--accent-hover);'}">${ds.status}</span>
</div>
`).join('');
} catch (e) {
datasetHistory.innerHTML = '<span style="color:var(--text-muted);">Failed to load history.</span>';
}
}
async function loadDatasetResult(jobId) {
try {
const res = await fetch(`${API_BASE}/api/dataset/job/${jobId}`);
const data = await res.json();
if (data.status === 'completed' && data.results) {
showEvaluationResults(data.results);
} else if (data.status === 'failed') {
showToast('Evaluation failed: ' + data.error);
} else if (data.status === 'running' || data.status === 'pending') {
datasetIdInput.value = data.dataset_id || '';
currentJobId = jobId;
saveJobId(currentJobId);
datasetLoading.style.display = 'block';
evaluateDatasetBtn.disabled = true;
if (data.progress) {
datasetLoadingText.textContent = `${data.progress.stage === 'downloading' ? 'Downloading' : data.progress.stage === 'evaluating' ? 'Evaluating' : 'Processing'}: ${data.progress.percent}%`;
} else {
datasetLoadingText.textContent = 'Evaluation running, please wait...';
}
startJobPolling();
}
} catch (e) {
showToast('Error: ' + e.message);
}
}
function showEvaluationResults(data) {
document.getElementById('evalTotal').textContent = data.extracted_count?.toLocaleString() || '-';
document.getElementById('evalLikelyProvider').textContent = data.likely_provider || '-';
document.getElementById('evalAvgConfidence').textContent = (data.average_confidence || 0) + '%';
const distContainer = document.getElementById('providerDistribution');
distContainer.innerHTML = '';
const sortedProviders = Object.entries(data.provider_counts || {})
.sort((a, b) => b[1].count - a[1].count);
for (const [provider, info] of sortedProviders) {
const conf = data.provider_confidences?.[provider]?.average || 0;
const html = `
<div style="margin-bottom:1rem;">
<div style="display:flex;justify-content:space-between;margin-bottom:0.25rem;">
<span style="font-weight:500;">${provider}</span>
<span style="color:var(--text-secondary);font-size:0.85rem;">${info.count} (${info.percentage}%) · ${conf}% avg</span>
</div>
<div class="result-bar">
<div class="result-bar-fill" style="width:${info.percentage}%"></div>
</div>
</div>
`;
distContainer.innerHTML += html;
}
const topContainer = document.getElementById('topProvidersList');
topContainer.innerHTML = '';
const sortedTop = Object.entries(data.top_providers || {})
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
for (const [provider, count] of sortedTop) {
const conf = data.provider_confidences?.[provider]?.cumulative || 0;
topContainer.innerHTML += `
<div class="result-item">
<span class="result-name">${provider}</span>
<span class="result-percent">${conf.toFixed(2)} pts</span>
</div>
`;
}
datasetResults.style.display = 'block';
datasetLoading.style.display = 'none';
}
function startJobPolling() {
if (jobPollingInterval) clearInterval(jobPollingInterval);
jobPollingInterval = setInterval(async () => {
if (!currentJobId) return;
try {
const res = await fetch(`${API_BASE}/api/dataset/job/${currentJobId}`);
const data = await res.json();
console.log('Polling response:', data);
if (data.status === 'completed') {
clearInterval(jobPollingInterval);
jobPollingInterval = null;
currentJobId = null;
clearSavedJobId();
showEvaluationResults(data.results);
loadDatasetHistory();
showToast('Evaluation complete!', 'success');
} else if (data.status === 'failed') {
clearInterval(jobPollingInterval);
jobPollingInterval = null;
currentJobId = null;
clearSavedJobId();
datasetLoading.style.display = 'none';
evaluateDatasetBtn.disabled = false;
showToast('Evaluation failed: ' + data.error);
} else {
const prog = data.progress;
if (prog) {
datasetLoadingText.textContent = `${prog.stage === 'downloading' ? 'Downloading' : prog.stage === 'evaluating' ? 'Evaluating' : 'Processing'}: ${prog.percent}%`;
} else {
datasetLoadingText.textContent = 'Evaluating... ' + (data.started_at ? new Date(data.started_at).toLocaleTimeString() : '');
}
}
} catch (e) {
console.error('Polling error:', e);
}
}, 2000);
}
async function checkDatasetFormat() {
const datasetId = datasetIdInput.value.trim();
if (!datasetId) {
showToast('Please enter a dataset ID');
return;
}
checkDatasetBtn.disabled = true;
checkDatasetBtn.innerHTML = '<span class="loading"></span>';
const customFormat = buildFormatString();
try {
const res = await fetch(`${API_BASE}/api/dataset/info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dataset_id: datasetId,
max_samples: parseInt(maxSamplesInput.value) || 1000,
custom_format: customFormat
})
});
const data = await res.json();
currentDatasetInfo = data;
const formatDetectedButNoTexts = data.supported && (data.extracted_count === 0);
if (data.supported && !formatDetectedButNoTexts) {
formatName.textContent = data.format_name || data.format || 'Unknown';
formatDescription.textContent = data.format_description || '';
totalRowsEl.textContent = data.total_rows?.toLocaleString() || '-';
extractedCountEl.textContent = data.extracted_count?.toLocaleString() || '-';
formatError.style.display = 'none';
evaluateDatasetBtn.disabled = false;
} else {
if (formatDetectedButNoTexts) {
formatName.textContent = data.format_name || data.format || 'Unknown';
formatDescription.textContent = 'Format detected but no valid assistant responses found. Try a custom format below.';
totalRowsEl.textContent = data.total_rows?.toLocaleString() || '-';
extractedCountEl.textContent = '0';
formatError.style.display = 'block';
formatError.textContent = 'No valid assistant responses extracted (minimum 50 chars required). The detected format may not match the actual data structure.';
} else {
formatName.textContent = 'Unsupported Format';
formatDescription.textContent = '';
totalRowsEl.textContent = '-';
extractedCountEl.textContent = '-';
formatError.style.display = 'block';
formatError.textContent = data.error || 'Unknown error';
}
evaluateDatasetBtn.disabled = true;
useCustomFormatCheckbox.checked = true;
customFormatSection.style.display = 'block';
showToast('Could not extract responses. Please specify a custom format below.');
}
datasetFormatInfo.style.display = 'block';
datasetResults.style.display = 'none';
} catch (e) {
showToast('Error: ' + e.message);
} finally {
checkDatasetBtn.disabled = false;
checkDatasetBtn.textContent = 'Check Format';
}
}
async function evaluateDataset() {
const datasetId = datasetIdInput.value.trim();
if (!datasetId || !currentDatasetInfo?.supported) return;
evaluateDatasetBtn.disabled = true;
datasetLoading.style.display = 'block';
datasetResults.style.display = 'none';
datasetLoadingText.textContent = 'Starting evaluation...';
const apiKey = getApiKey();
const customFormat = buildFormatString();
try {
const res = await fetch(`${API_BASE}/api/dataset/evaluate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dataset_id: datasetId,
max_samples: parseInt(maxSamplesInput.value) || 1000,
api_key: apiKey || null,
custom_format: customFormat
})
});
const data = await res.json();
console.log('Evaluate response:', data);
if (data.error) {
showToast(data.error);
datasetLoading.style.display = 'none';
evaluateDatasetBtn.disabled = false;
return;
}
currentJobId = data.job_id;
saveJobId(currentJobId);
console.log('Job ID saved:', currentJobId);
datasetLoadingText.textContent = 'Evaluation started. Processing in background...';
// Show info that user can close the page
const closePageMsg = document.createElement('div');
closePageMsg.style.cssText = 'margin-top:1rem;color:var(--text-muted);font-size:0.85rem;';
closePageMsg.innerHTML = '✓ You can close this page — evaluation will continue in the background.';
const loadingEl = document.getElementById('datasetLoading');
loadingEl.querySelectorAll('.close-page-msg').forEach(el => el.remove());
closePageMsg.className = 'close-page-msg';
loadingEl.appendChild(closePageMsg);
startJobPolling();
loadDatasetHistory();
} catch (e) {
showToast('Error: ' + e.message);
datasetLoading.style.display = 'none';
evaluateDatasetBtn.disabled = false;
}
}
checkDatasetBtn.addEventListener('click', checkDatasetFormat);
evaluateDatasetBtn.addEventListener('click', evaluateDataset);
document.getElementById('clearHistoryBtn').addEventListener('click', async () => {
if (!confirm('Clear all dataset evaluation history?')) return;
try {
const res = await fetch(`${API_BASE}/api/datasets/clear`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: getApiKey() })
});
const data = await res.json();
if (data.error) {
showToast(data.error);
} else {
clearSavedJobId();
showToast(`Cleared ${data.cleared} datasets`, 'success');
loadDatasetHistory();
}
} catch (e) {
showToast('Error: ' + e.message);
}
});
datasetIdInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') checkDatasetFormat();
});
loadDatasetHistory();
// Load supported formats
async function loadSupportedFormats() {
try {
const res = await fetch(`${API_BASE}/api/dataset/formats`);
const data = await res.json();
const container = document.getElementById('supportedFormatsList');
container.innerHTML = data.formats.map(f => `
<div style="background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:0.75rem;">
<div style="font-weight:500;font-size:0.85rem;">${f.name}</div>
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem;">${f.description}</div>
</div>
`).join('');
} catch (e) {
console.error('Failed to load formats:', e);
}
}
// ── Custom Format UI Handling ──
const useCustomFormatCheckbox = document.getElementById('useCustomFormat');
const customFormatSection = document.getElementById('customFormatSection');
const formatPreview = document.getElementById('formatPreview');
const columnInput = document.getElementById('columnInput');
const twoColumnInput = document.getElementById('twoColumnInput');
const patternInput = document.getElementById('patternInput');
const customColumnName = document.getElementById('customColumnName');
const customUserColumn = document.getElementById('customUserColumn');
const customAssistantColumn = document.getElementById('customAssistantColumn');
const customPattern = document.getElementById('customPattern');
function buildFormatString() {
if (!useCustomFormatCheckbox.checked) return null;
const formatType = document.querySelector('input[name="customFormatType"]:checked')?.value || 'auto';
if (formatType === 'auto') return null;
if (formatType === 'column') {
const col = customColumnName.value.trim();
return col ? `column: ${col}` : null;
}
if (formatType === 'two_column') {
const userCol = customUserColumn.value.trim();
const assistantCol = customAssistantColumn.value.trim();
if (assistantCol) {
return userCol ? `column: ${userCol}, column: ${assistantCol}` : `column: ${assistantCol}`;
}
return null;
}
if (formatType === 'pattern') {
const pat = customPattern.value.trim();
if (!pat) return null;
if (pat.includes('[startuser]') && pat.includes('[startassistant]')) {
return pat;
}
const parts = pat.split(/\s+/);
if (parts.length >= 2) {
return `pattern: ${parts[0]}, pattern: ${parts[1]}`;
}
return `column: ${pat}`;
}
return null;
}
function updateFormatPreview() {
const fmt = buildFormatString();
formatPreview.textContent = fmt || '(auto-detect)';
formatPreview.style.color = fmt ? 'var(--accent)' : 'var(--text-muted)';
}
useCustomFormatCheckbox?.addEventListener('change', () => {
customFormatSection.style.display = useCustomFormatCheckbox.checked ? 'block' : 'none';
updateFormatPreview();
});
document.querySelectorAll('input[name="customFormatType"]').forEach(radio => {
radio.addEventListener('change', (e) => {
columnInput.style.display = e.target.value === 'column' ? 'block' : 'none';
twoColumnInput.style.display = e.target.value === 'two_column' ? 'block' : 'none';
patternInput.style.display = e.target.value === 'pattern' ? 'block' : 'none';
updateFormatPreview();
});
});
[customColumnName, customUserColumn, customAssistantColumn, customPattern].forEach(input => {
input?.addEventListener('input', updateFormatPreview);
});
checkStatus();
async function restoreJobState() {
const savedJobId = getSavedJobId();
if (!savedJobId) return;
console.log('Restoring job state, savedJobId:', savedJobId);
try {
const res = await fetch(`${API_BASE}/api/dataset/job/${savedJobId}`);
const data = await res.json();
console.log('Job data:', data);
if (data.status === 'running' || data.status === 'pending') {
currentJobId = savedJobId;
datasetIdInput.value = data.dataset_id || '';
datasetLoading.style.display = 'block';
evaluateDatasetBtn.disabled = true;
const prog = data.progress;
console.log('Progress:', prog);
if (prog) {
datasetLoadingText.textContent = `${prog.stage === 'downloading' ? 'Downloading' : prog.stage === 'evaluating' ? 'Evaluating' : 'Processing'}: ${prog.percent}%`;
} else {
datasetLoadingText.textContent = 'Starting evaluation...';
}
startJobPolling();
} else if (data.status === 'completed') {
clearSavedJobId();
showEvaluationResults(data.results);
} else if (data.status === 'failed') {
clearSavedJobId();
}
} catch (e) {
console.error('Restore error:', e);
clearSavedJobId();
}
}
restoreJobState();
</script>
</body>
</html>