Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Wellfound AI - Excel Data Completion</title> | |
| <style> | |
| :root { | |
| --bg: #f8fafc; | |
| --surface: #ffffff; | |
| --surface2: #f1f5f9; | |
| --border: #e2e8f0; | |
| --text: #0f172a; | |
| --text2: #475569; | |
| --text3: #94a3b8; | |
| --primary: #2563eb; | |
| --primary-hover: #1d4ed8; | |
| --primary-light: #dbeafe; | |
| --success: #16a34a; | |
| --success-light: #dcfce7; | |
| --warning: #d97706; | |
| --warning-light: #fef3c7; | |
| --danger: #dc2626; | |
| --danger-light: #fee2e2; | |
| --radius: 10px; | |
| --radius-sm: 6px; | |
| --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06); | |
| --shadow-md: 0 4px 6px rgba(0,0,0,.07), 0 2px 4px rgba(0,0,0,.06); | |
| --shadow-lg: 0 10px 15px rgba(0,0,0,.1), 0 4px 6px rgba(0,0,0,.05); | |
| --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| --mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: var(--font); | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| } | |
| /* βββ Layout βββ */ | |
| .app-container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 24px 20px 60px; | |
| } | |
| /* βββ Header βββ */ | |
| .app-header { | |
| text-align: center; | |
| padding: 32px 0 40px; | |
| } | |
| .app-header .logo { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .app-header .logo-icon { | |
| width: 42px; | |
| height: 42px; | |
| background: linear-gradient(135deg, #2563eb, #7c3aed); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 22px; | |
| color: #fff; | |
| } | |
| .app-header h1 { | |
| font-size: 28px; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .app-header .subtitle { | |
| color: var(--text2); | |
| font-size: 15px; | |
| margin-top: 4px; | |
| } | |
| /* βββ Cards βββ */ | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 24px; | |
| margin-bottom: 20px; | |
| box-shadow: var(--shadow); | |
| } | |
| .card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| padding-bottom: 14px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .card-header .step-num { | |
| width: 28px; | |
| height: 28px; | |
| background: var(--primary-light); | |
| color: var(--primary); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 13px; | |
| font-weight: 700; | |
| flex-shrink: 0; | |
| } | |
| .card-header h2 { | |
| font-size: 17px; | |
| font-weight: 600; | |
| } | |
| /* βββ Form Elements βββ */ | |
| .form-group { | |
| margin-bottom: 16px; | |
| } | |
| .form-group:last-child { margin-bottom: 0; } | |
| .form-label { | |
| display: block; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text2); | |
| margin-bottom: 6px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .form-input, .form-select { | |
| width: 100%; | |
| padding: 10px 14px; | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| font-size: 14px; | |
| font-family: var(--font); | |
| background: var(--surface); | |
| color: var(--text); | |
| transition: border-color .15s, box-shadow .15s; | |
| outline: none; | |
| } | |
| .form-input:focus, .form-select:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(37,99,235,.15); | |
| } | |
| .form-input::placeholder { color: var(--text3); } | |
| .form-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 14px; | |
| } | |
| @media (max-width: 500px) { | |
| .form-row { grid-template-columns: 1fr; } | |
| } | |
| /* βββ Toggle Switch βββ */ | |
| .toggle-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .toggle-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .toggle-track { | |
| width: 44px; | |
| height: 26px; | |
| background: #cbd5e1; | |
| border-radius: 13px; | |
| position: relative; | |
| transition: background .2s; | |
| } | |
| .toggle-track.active { | |
| background: var(--primary); | |
| } | |
| .toggle-thumb { | |
| width: 20px; | |
| height: 20px; | |
| background: #fff; | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| transition: transform .2s; | |
| box-shadow: 0 1px 3px rgba(0,0,0,.2); | |
| } | |
| .toggle-track.active .toggle-thumb { | |
| transform: translateX(18px); | |
| } | |
| .toggle-label { | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| /* βββ Upload Zone βββ */ | |
| .upload-zone { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius); | |
| padding: 40px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all .2s; | |
| background: var(--surface2); | |
| } | |
| .upload-zone:hover, .upload-zone.drag-over { | |
| border-color: var(--primary); | |
| background: var(--primary-light); | |
| } | |
| .upload-zone .upload-icon { | |
| font-size: 36px; | |
| margin-bottom: 10px; | |
| } | |
| .upload-zone .upload-text { | |
| font-size: 15px; | |
| font-weight: 500; | |
| color: var(--text2); | |
| } | |
| .upload-zone .upload-hint { | |
| font-size: 12px; | |
| color: var(--text3); | |
| margin-top: 4px; | |
| } | |
| .upload-zone input[type=file] { display: none; } | |
| .file-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 14px; | |
| background: var(--success-light); | |
| border-radius: var(--radius-sm); | |
| margin-top: 14px; | |
| } | |
| .file-info .file-icon { font-size: 24px; } | |
| .file-info .file-name { font-weight: 600; font-size: 14px; } | |
| .file-info .file-meta { font-size: 12px; color: var(--text2); } | |
| .file-info .file-remove { | |
| margin-left: auto; | |
| background: none; | |
| border: none; | |
| color: var(--danger); | |
| cursor: pointer; | |
| font-size: 18px; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| } | |
| .file-info .file-remove:hover { background: var(--danger-light); } | |
| /* βββ Buttons βββ */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 11px 24px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| border-radius: var(--radius-sm); | |
| border: none; | |
| cursor: pointer; | |
| transition: all .15s; | |
| font-family: var(--font); | |
| white-space: nowrap; | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: #fff; | |
| } | |
| .btn-primary:hover { background: var(--primary-hover); } | |
| .btn-primary:disabled { | |
| background: #93c5fd; | |
| cursor: not-allowed; | |
| } | |
| .btn-outline { | |
| background: var(--surface); | |
| color: var(--primary); | |
| border: 1.5px solid var(--primary); | |
| } | |
| .btn-outline:hover { background: var(--primary-light); } | |
| .btn-success { | |
| background: var(--success); | |
| color: #fff; | |
| } | |
| .btn-success:hover { opacity: .9; } | |
| .btn-lg { padding: 13px 32px; font-size: 15px; } | |
| .btn-block { width: 100%; } | |
| /* βββ Progress βββ */ | |
| .progress-section { margin-top: 18px; } | |
| .progress-bar-outer { | |
| width: 100%; | |
| height: 10px; | |
| background: var(--surface2); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| } | |
| .progress-bar-inner { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), #7c3aed); | |
| border-radius: 5px; | |
| transition: width .3s; | |
| width: 0%; | |
| } | |
| .progress-stats { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 8px; | |
| font-size: 13px; | |
| color: var(--text2); | |
| } | |
| .progress-current { | |
| font-size: 13px; | |
| color: var(--text3); | |
| margin-top: 6px; | |
| font-family: var(--mono); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| /* βββ Status Badges βββ */ | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 3px 10px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: .3px; | |
| } | |
| .badge-success { background: var(--success-light); color: var(--success); } | |
| .badge-warning { background: var(--warning-light); color: var(--warning); } | |
| .badge-error { background: var(--danger-light); color: var(--danger); } | |
| .badge-info { background: var(--primary-light); color: var(--primary); } | |
| /* βββ Error List βββ */ | |
| .error-list { | |
| margin-top: 14px; | |
| max-height: 180px; | |
| overflow-y: auto; | |
| background: var(--danger-light); | |
| border-radius: var(--radius-sm); | |
| padding: 10px; | |
| } | |
| .error-item { | |
| font-size: 12px; | |
| color: var(--danger); | |
| padding: 3px 0; | |
| font-family: var(--mono); | |
| border-bottom: 1px solid rgba(220,38,38,.1); | |
| } | |
| .error-item:last-child { border-bottom: none; } | |
| /* βββ Steps βββ */ | |
| .steps-indicator { | |
| display: flex; | |
| gap: 4px; | |
| margin-bottom: 24px; | |
| } | |
| .step-dot { | |
| flex: 1; | |
| height: 3px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| transition: background .3s; | |
| } | |
| .step-dot.done { background: var(--success); } | |
| .step-dot.active { background: var(--primary); } | |
| /* βββ Data Preview βββ */ | |
| .preview-table-wrapper { | |
| overflow-x: auto; | |
| margin-top: 14px; | |
| } | |
| .preview-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 12px; | |
| } | |
| .preview-table th { | |
| background: var(--surface2); | |
| padding: 8px 10px; | |
| text-align: left; | |
| font-weight: 600; | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: .3px; | |
| color: var(--text2); | |
| border-bottom: 2px solid var(--border); | |
| white-space: nowrap; | |
| } | |
| .preview-table td { | |
| padding: 7px 10px; | |
| border-bottom: 1px solid var(--border); | |
| max-width: 150px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .preview-table .null-cell { color: var(--text3); font-style: italic; } | |
| /* βββ Deployment Info βββ */ | |
| .deploy-info { | |
| margin-top: 18px; | |
| padding: 16px; | |
| background: var(--surface2); | |
| border-radius: var(--radius-sm); | |
| font-size: 13px; | |
| color: var(--text2); | |
| } | |
| .deploy-info h3 { | |
| font-size: 14px; | |
| margin-bottom: 8px; | |
| } | |
| .deploy-info table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 12px; | |
| } | |
| .deploy-info th, .deploy-info td { | |
| padding: 6px 10px; | |
| border: 1px solid var(--border); | |
| text-align: left; | |
| } | |
| .deploy-info th { background: var(--surface); font-weight: 600; } | |
| .deploy-info .best { background: #dcfce7; font-weight: 600; } | |
| /* βββ Toast βββ */ | |
| .toast { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 12px 20px; | |
| border-radius: var(--radius-sm); | |
| font-size: 14px; | |
| font-weight: 500; | |
| box-shadow: var(--shadow-lg); | |
| z-index: 1000; | |
| animation: slideIn .3s ease; | |
| max-width: 400px; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| .toast-success { background: var(--success-light); color: var(--success); border: 1px solid var(--success); } | |
| .toast-error { background: var(--danger-light); color: var(--danger); border: 1px solid var(--danger); } | |
| /* βββ Hidden βββ */ | |
| .hidden { display: none ; } | |
| /* βββ Responsive βββ */ | |
| @media (max-width: 600px) { | |
| .app-container { padding: 16px 12px; } | |
| .app-header h1 { font-size: 22px; } | |
| .card { padding: 16px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header class="app-header"> | |
| <div class="logo"> | |
| <div class="logo-icon">π€</div> | |
| <h1>Wellfound AI</h1> | |
| </div> | |
| <p class="subtitle">AI-Powered Excel Data Completion Tool</p> | |
| </header> | |
| <!-- Steps indicator --> | |
| <div class="steps-indicator" id="stepsIndicator"> | |
| <div class="step-dot done"></div> | |
| <div class="step-dot" id="stepDot2"></div> | |
| <div class="step-dot" id="stepDot3"></div> | |
| <div class="step-dot" id="stepDot4"></div> | |
| </div> | |
| <!-- Step 1: AI Configuration --> | |
| <div class="card" id="cardConfig"> | |
| <div class="card-header"> | |
| <span class="step-num">1</span> | |
| <h2>AI Configuration</h2> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label class="form-label">AI Service Provider</label> | |
| <select class="form-select" id="providerSelect"> | |
| <option value="openai">OpenAI (GPT-4o)</option> | |
| <option value="deepseek">DeepSeek (DeepSeek-Chat)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Model</label> | |
| <select class="form-select" id="modelSelect"> | |
| <option value="auto">Auto (Best Available)</option> | |
| <option value="gpt-4o">GPT-4o</option> | |
| <option value="gpt-4o-mini">GPT-4o-mini (Fast)</option> | |
| <option value="deepseek-chat">DeepSeek-Chat</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">API Key</label> | |
| <input type="password" class="form-input" id="apiKeyInput" | |
| placeholder="sk-... (OpenAI) or sk-... (DeepSeek)" | |
| autocomplete="off"> | |
| </div> | |
| <div class="toggle-group"> | |
| <label class="toggle-item" id="toggleAi"> | |
| <div class="toggle-track active" id="toggleAiTrack"> | |
| <div class="toggle-thumb"></div> | |
| </div> | |
| <span class="toggle-label">AI Extraction</span> | |
| </label> | |
| <label class="toggle-item" id="toggleWeb"> | |
| <div class="toggle-track active" id="toggleWebTrack"> | |
| <div class="toggle-thumb"></div> | |
| </div> | |
| <span class="toggle-label">Web Scraping</span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Step 2: File Upload --> | |
| <div class="card" id="cardUpload"> | |
| <div class="card-header"> | |
| <span class="step-num">2</span> | |
| <h2>Upload Excel File</h2> | |
| </div> | |
| <div class="upload-zone" id="uploadZone"> | |
| <div class="upload-icon">π</div> | |
| <div class="upload-text">Drag & drop your Excel file here</div> | |
| <div class="upload-hint">Supports .xlsx, .xls (Wellfound export format)</div> | |
| <input type="file" id="fileInput" accept=".xlsx,.xls"> | |
| </div> | |
| <div class="file-info hidden" id="fileInfo"> | |
| <span class="file-icon">π</span> | |
| <div> | |
| <div class="file-name" id="fileName"></div> | |
| <div class="file-meta" id="fileMeta"></div> | |
| </div> | |
| <button class="file-remove" id="fileRemove" title="Remove file">×</button> | |
| </div> | |
| <div class="preview-table-wrapper hidden" id="previewSection"> | |
| <table class="preview-table" id="previewTable"> | |
| <thead id="previewHead"></thead> | |
| <tbody id="previewBody"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Step 3: Process --> | |
| <div class="card" id="cardProcess"> | |
| <div class="card-header"> | |
| <span class="step-num">3</span> | |
| <h2>Process Data</h2> | |
| <span id="statusBadge" class="hidden"></span> | |
| </div> | |
| <div style="display:flex;gap:12px;flex-wrap:wrap;"> | |
| <div class="form-group" style="flex:1;min-width:120px;"> | |
| <label class="form-label">Start Row</label> | |
| <input type="number" class="form-input" id="startRowInput" value="0" min="0"> | |
| </div> | |
| <div class="form-group" style="flex:1;min-width:120px;"> | |
| <label class="form-label">Max Rows (0=All)</label> | |
| <input type="number" class="form-input" id="maxRowsInput" value="0" min="0"> | |
| </div> | |
| <div class="form-group" style="flex:1;min-width:120px;"> | |
| <label class="form-label">Concurrency</label> | |
| <input type="number" class="form-input" id="concurrencyInput" value="2" min="1" max="5"> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary btn-lg btn-block" id="btnStart" disabled> | |
| β‘ Start Processing | |
| </button> | |
| <!-- Progress --> | |
| <div class="progress-section hidden" id="progressSection"> | |
| <div class="progress-bar-outer"> | |
| <div class="progress-bar-inner" id="progressBar"></div> | |
| </div> | |
| <div class="progress-stats"> | |
| <span id="progressText">0 / 0 processed</span> | |
| <span id="progressPct">0%</span> | |
| </div> | |
| <div class="progress-current" id="progressCurrent"></div> | |
| </div> | |
| <!-- Errors --> | |
| <div class="error-list hidden" id="errorList"></div> | |
| </div> | |
| <!-- Step 4: Download --> | |
| <div class="card hidden" id="cardDownload"> | |
| <div class="card-header"> | |
| <span class="step-num">4</span> | |
| <h2>Download Results</h2> | |
| </div> | |
| <button class="btn btn-success btn-lg btn-block" id="btnDownload"> | |
| β¬ Download Processed Excel | |
| </button> | |
| </div> | |
| <!-- Deployment Evaluation --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="step-num">π</span> | |
| <h2>Deployment Evaluation</h2> | |
| </div> | |
| <div class="deploy-info"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Criteria</th> | |
| <th>Hugging Face Space</th> | |
| <th>n8n Workflow</th> | |
| <th>GitHub Open Source</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><strong>File Processing</strong></td> | |
| <td class="best">β Persistent storage, up to 50GB</td> | |
| <td>β Limited file handling</td> | |
| <td>β Self-hosted, unlimited</td> | |
| </tr> | |
| <tr> | |
| <td><strong>Deployment Ease</strong></td> | |
| <td class="best">β Git push / GUI, 1-click</td> | |
| <td>β Requires n8n instance</td> | |
| <td>β Manual setup required</td> | |
| </tr> | |
| <tr> | |
| <td><strong>Entry Barrier</strong></td> | |
| <td class="best">β Low, browser-based</td> | |
| <td>β Medium, workflow knowledge</td> | |
| <td>β High, dev skills needed</td> | |
| </tr> | |
| <tr> | |
| <td><strong>Web Scraping</strong></td> | |
| <td class="best">β Playwright supported</td> | |
| <td>β HTTP nodes only</td> | |
| <td>β Full control</td> | |
| </tr> | |
| <tr> | |
| <td><strong>Cost</strong></td> | |
| <td class="best">β Free tier available</td> | |
| <td>β Self-host or paid</td> | |
| <td>β Free (own infra)</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <p style="margin-top:12px;font-weight:600;color:var(--success);"> | |
| π Best Choice: <strong>Hugging Face Space</strong> β optimal balance of ease, features, and cost | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // βββ State βββ | |
| const state = { | |
| fileId: null, | |
| filename: null, | |
| taskId: null, | |
| useAi: true, | |
| useWeb: true, | |
| pollingInterval: null, | |
| }; | |
| // βββ DOM Elements βββ | |
| const $ = (sel) => document.querySelector(sel); | |
| const $$ = (sel) => document.querySelectorAll(sel); | |
| const uploadZone = $('#uploadZone'); | |
| const fileInput = $('#fileInput'); | |
| const fileInfo = $('#fileInfo'); | |
| const fileName = $('#fileName'); | |
| const fileMeta = $('#fileMeta'); | |
| const fileRemove = $('#fileRemove'); | |
| const previewSection = $('#previewSection'); | |
| const previewHead = $('#previewHead'); | |
| const previewBody = $('#previewBody'); | |
| const btnStart = $('#btnStart'); | |
| const btnDownload = $('#btnDownload'); | |
| const progressSection = $('#progressSection'); | |
| const progressBar = $('#progressBar'); | |
| const progressText = $('#progressText'); | |
| const progressPct = $('#progressPct'); | |
| const progressCurrent = $('#progressCurrent'); | |
| const errorList = $('#errorList'); | |
| const statusBadge = $('#statusBadge'); | |
| const cardDownload = $('#cardDownload'); | |
| const apiKeyInput = $('#apiKeyInput'); | |
| const providerSelect = $('#providerSelect'); | |
| const modelSelect = $('#modelSelect'); | |
| // βββ Toggle Switches βββ | |
| function setupToggle(toggleId, trackId, stateKey) { | |
| const toggle = document.getElementById(toggleId); | |
| const track = document.getElementById(trackId); | |
| toggle.addEventListener('click', () => { | |
| state[stateKey] = !state[stateKey]; | |
| if (state[stateKey]) { | |
| track.classList.add('active'); | |
| } else { | |
| track.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| setupToggle('toggleAi', 'toggleAiTrack', 'useAi'); | |
| setupToggle('toggleWeb', 'toggleWebTrack', 'useWeb'); | |
| // Update model options based on provider | |
| providerSelect.addEventListener('change', () => { | |
| const provider = providerSelect.value; | |
| modelSelect.innerHTML = '<option value="auto">Auto (Best Available)</option>'; | |
| if (provider === 'openai') { | |
| modelSelect.innerHTML += ` | |
| <option value="gpt-4o">GPT-4o</option> | |
| <option value="gpt-4o-mini">GPT-4o-mini (Fast)</option> | |
| <option value="gpt-4-turbo">GPT-4 Turbo</option> | |
| `; | |
| } else { | |
| modelSelect.innerHTML += ` | |
| <option value="deepseek-chat">DeepSeek-Chat</option> | |
| <option value="deepseek-reasoner">DeepSeek-Reasoner</option> | |
| `; | |
| } | |
| }); | |
| // βββ File Upload βββ | |
| uploadZone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| uploadZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.add('drag-over'); | |
| }); | |
| uploadZone.addEventListener('dragleave', () => { | |
| uploadZone.classList.remove('drag-over'); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.remove('drag-over'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| fileInput.files = files; | |
| handleFileSelect(); | |
| } | |
| }); | |
| fileRemove.addEventListener('click', () => { | |
| state.fileId = null; | |
| state.filename = null; | |
| fileInput.value = ''; | |
| fileInfo.classList.add('hidden'); | |
| previewSection.classList.add('hidden'); | |
| btnStart.disabled = true; | |
| cardDownload.classList.add('hidden'); | |
| }); | |
| async function handleFileSelect() { | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const res = await fetch('/api/upload', { method: 'POST', body: formData }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| showToast(err.detail || 'Upload failed', 'error'); | |
| return; | |
| } | |
| const data = await res.json(); | |
| state.fileId = data.file_id; | |
| state.filename = data.filename; | |
| fileName.textContent = data.filename; | |
| fileMeta.textContent = `${data.rows} rows, ${data.columns.length} columns | Missing data in ${Object.keys(data.missing_data).length} columns`; | |
| fileInfo.classList.remove('hidden'); | |
| btnStart.disabled = false; | |
| // Show column preview | |
| previewHead.innerHTML = '<tr>' + data.columns.map(c => `<th>${c}</th>`).join('') + '</tr>'; | |
| previewSection.classList.remove('hidden'); | |
| showToast(`File loaded: ${data.rows} rows`, 'success'); | |
| } catch (err) { | |
| showToast('Upload failed: ' + err.message, 'error'); | |
| } | |
| } | |
| // βββ Start Processing βββ | |
| btnStart.addEventListener('click', startProcessing); | |
| async function startProcessing() { | |
| if (!state.fileId) return; | |
| const apiKey = apiKeyInput.value.trim(); | |
| if (state.useAi && !apiKey) { | |
| showToast('Please enter your API key', 'error'); | |
| apiKeyInput.focus(); | |
| return; | |
| } | |
| btnStart.disabled = true; | |
| btnStart.textContent = 'β³ Starting...'; | |
| progressSection.classList.remove('hidden'); | |
| errorList.classList.add('hidden'); | |
| errorList.innerHTML = ''; | |
| cardDownload.classList.add('hidden'); | |
| const payload = { | |
| file_id: state.fileId, | |
| api_key: apiKey, | |
| provider: providerSelect.value, | |
| model: modelSelect.value, | |
| start_row: parseInt($('#startRowInput').value) || 0, | |
| max_rows: parseInt($('#maxRowsInput').value) || 0, | |
| use_ai: state.useAi, | |
| use_web_scraping: state.useWeb, | |
| concurrency: parseInt($('#concurrencyInput').value) || 2, | |
| }; | |
| try { | |
| const res = await fetch('/api/process', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| showToast(err.detail || 'Failed to start', 'error'); | |
| btnStart.disabled = false; | |
| btnStart.textContent = 'β‘ Start Processing'; | |
| return; | |
| } | |
| const data = await res.json(); | |
| state.taskId = data.task_id; | |
| showToast('Processing started!', 'success'); | |
| startPolling(); | |
| } catch (err) { | |
| showToast('Error: ' + err.message, 'error'); | |
| btnStart.disabled = false; | |
| btnStart.textContent = 'β‘ Start Processing'; | |
| } | |
| } | |
| // βββ Polling βββ | |
| function startPolling() { | |
| if (state.pollingInterval) clearInterval(state.pollingInterval); | |
| state.pollingInterval = setInterval(async () => { | |
| try { | |
| const res = await fetch(`/api/status/${state.taskId}`); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| updateProgress(data); | |
| if (data.status === 'completed' || data.status === 'error') { | |
| clearInterval(state.pollingInterval); | |
| state.pollingInterval = null; | |
| if (data.status === 'completed') { | |
| showToast('Processing complete!', 'success'); | |
| cardDownload.classList.remove('hidden'); | |
| btnDownload.onclick = () => { | |
| window.location.href = `/api/download/${state.taskId}`; | |
| }; | |
| statusBadge.textContent = 'Complete'; | |
| statusBadge.className = 'badge badge-success'; | |
| statusBadge.classList.remove('hidden'); | |
| } else { | |
| statusBadge.textContent = 'Error'; | |
| statusBadge.className = 'badge badge-error'; | |
| statusBadge.classList.remove('hidden'); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Poll error:', err); | |
| } | |
| }, 1000); | |
| } | |
| function updateProgress(data) { | |
| const pct = data.total > 0 ? Math.round((data.progress / data.total) * 100) : 0; | |
| progressBar.style.width = pct + '%'; | |
| progressText.textContent = `${data.progress} / ${data.total} processed`; | |
| progressPct.textContent = pct + '%'; | |
| if (data.current) { | |
| progressCurrent.textContent = data.current; | |
| } | |
| // Status badge | |
| if (data.status === 'processing') { | |
| statusBadge.textContent = 'Processing'; | |
| statusBadge.className = 'badge badge-info'; | |
| statusBadge.classList.remove('hidden'); | |
| btnStart.textContent = 'β³ Processing...'; | |
| } | |
| // Errors | |
| if (data.errors && data.errors.length > 0) { | |
| errorList.classList.remove('hidden'); | |
| errorList.innerHTML = data.errors.map(e => | |
| `<div class="error-item">β ${escapeHtml(e)}</div>` | |
| ).join(''); | |
| } | |
| } | |
| // βββ Helpers βββ | |
| function showToast(message, type) { | |
| const existing = document.querySelector('.toast'); | |
| if (existing) existing.remove(); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast toast-${type}`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transition = 'opacity .3s'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 4000); | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| </script> | |
| </body> | |
| </html> | |