| <!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; |
| } |
| |
| .format-option:hover { |
| border-color: var(--border-light) !important; |
| background: var(--bg-elevated) !important; |
| } |
| |
| .format-option:has(input:checked) { |
| border-color: var(--accent-muted) !important; |
| background: rgba(232, 93, 4, 0.08) !important; |
| } |
| |
| @media (max-width: 600px) { |
| .container { |
| padding: 1rem; |
| } |
| |
| .logo { |
| font-size: 2rem; |
| } |
| |
| .btn-group { |
| flex-direction: column; |
| } |
| |
| .btn { |
| width: 100%; |
| } |
| |
| .result-main { |
| flex-direction: column; |
| gap: 0.5rem; |
| text-align: center; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <div class="logo">AI<span>Finder</span></div> |
| <p class="tagline">Identify which AI provider generated a response</p> |
| </header> |
| |
| <div class="tabs"> |
| <button class="tab active" data-tab="classify">Classify</button> |
| <button class="tab" data-tab="dataset">Evaluate Dataset</button> |
| <button class="tab" data-tab="docs">API Docs</button> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| </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(); |
| } |
| }); |
| |
| |
| 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(); |
| }; |
| |
| |
| 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...'; |
| |
| |
| 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(); |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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> |