Spaces:
Running
Running
| <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) ; | |
| background: var(--bg-elevated) ; | |
| } | |
| .format-option:has(input:checked) { | |
| border-color: var(--accent-muted) ; | |
| background: rgba(232, 93, 4, 0.08) ; | |
| } | |
| @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"><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> | |
| <!-- ── 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 <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> | |
| <!-- ── 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 — 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> |