| | <!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 { |
| | 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; |
| | } |
| | |
| | |
| | .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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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"><think>β¦</think></span> or |
| | <span class="docs-inline-code"><thinking>β¦</thinking></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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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 <think>/<thinking> blocks before classifying.""" |
| | return re.sub(r"<think(?:ing)?>.*?</think(?:ing)?>", |
| | "", 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']:<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(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/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 => { |
| | console.log(`Provider: ${data.provider} (${data.confidence}%)`); |
| | data.top_providers.forEach(p => |
| | 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(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/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 () => { |
| | const result = await classify( |
| | "Let me think about this step by step...", |
| | 3 |
| | ); |
| | console.log(result); |
| | })();</pre> |
| | </div> |
| | </div> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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 — 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(); |
| | } |
| | }); |
| | |
| | |
| | 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'); |
| | }); |
| | }); |
| | |
| | |
| | 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); |
| | }); |
| | } |
| | |
| | |
| | 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(''); |
| | } |
| | |
| | |
| | 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'; |
| | }); |
| | } |
| | |
| | |
| | const _origLoadProviders = loadProviders; |
| | loadProviders = async function() { |
| | await _origLoadProviders(); |
| | populateDocsProviders(); |
| | }; |
| | |
| | checkStatus(); |
| | </script> |
| | </body> |
| | </html> |
| |
|