AIFinder / static /index.html
CompactAI's picture
Upload 15 files
0051294 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;
}
@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="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>
</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="resetBtn">Reset Training</button>
</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>
<p style="margin-top: 0.5rem;">
Want to contribute? Test this and post to the
<a href="https://huggingface.co/spaces" target="_blank">HuggingFace Spaces Community</a>
if you want it merged!
</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 resetBtn = document.getElementById('resetBtn');
const toast = document.getElementById('toast');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
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 = `Ready (${data.device})`;
classifyBtn.disabled = false;
loadProviders();
loadStats();
} else {
setTimeout(checkStatus, 1000);
}
} catch (e) {
statusDot.classList.add('loading');
statusText.textContent = 'Connecting to API...';
setTimeout(checkStatus, 2000);
}
}
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++;
sessionCorrections++;
saveStats();
correctionsCountEl.textContent = correctionsCount;
sessionCountEl.textContent = sessionCorrections;
showToast(`Trained! Loss: ${data.loss.toFixed(4)}`, 'success');
stats.style.display = 'flex';
actions.style.display = 'flex';
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);
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();
};
checkStatus();
</script>
</body>
</html>