Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

*.DS_Store
*.idea
dataset
.venv
job_posting.csv
Expand All @@ -8,6 +9,4 @@ __pycache__/
.csv
app.py
parsed_resume.json
admin-scripts/bronze_jobs.json
admin-scripts/gold_jobs.json
admin-scripts/silver_jobs.json
admin-scripts*
181 changes: 181 additions & 0 deletions web/csv-lab/csv-lab-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* CSV Lab UI: registers the scoped service worker (HTTPS or localhost only).
* First install may require one reload so the page is controlled — see sessionStorage guard.
*/
(function () {
const swUrl = new URL('sw.js', window.location.href);
const scopeUrl = new URL('./', window.location.href).href;

const els = {
file: document.getElementById('csvFile'),
tableWrap: document.getElementById('tableWrap'),
meta: document.getElementById('meta'),
prompt: document.getElementById('sqlPrompt'),
error: document.getElementById('sqlError'),
swStatus: document.getElementById('swStatus'),
};

els.file.disabled = true;

function setError(msg) {
els.error.textContent = msg || '';
els.error.hidden = !msg;
}

function setSwStatus(text) {
els.swStatus.textContent = text;
}

function renderTable(data) {
const { columns, rows, rowCount, truncated } = data;
const total = rowCount != null ? rowCount : rows.length;
const truncNote = truncated ? ' (showing first rows only; full set is in the worker)' : '';
els.meta.textContent = `${total} row(s), ${columns.length} column(s)${truncNote}`;

if (!columns.length) {
els.tableWrap.innerHTML = '<p class="muted">No columns in CSV.</p>';
return;
}

const thead = `<thead><tr>${columns.map((c) => `<th>${escapeHtml(c)}</th>`).join('')}</tr></thead>`;
const tbody = `<tbody>${rows
.map(
(row) =>
`<tr>${columns.map((c) => `<td>${escapeHtml(row[c] ?? '')}</td>`).join('')}</tr>`
)
.join('')}</tbody>`;
els.tableWrap.innerHTML = `<table class="dataframe">${thead}${tbody}</table>`;
}

function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

function sendMessage(type, payload) {
return new Promise((resolve, reject) => {
const controller = navigator.serviceWorker.controller;
if (!controller) {
reject(new Error('Service worker is not controlling this page yet. Reload once and try again.'));
return;
}
const id = crypto.randomUUID();
const onMessage = (ev) => {
const d = ev.data;
if (!d || d.id !== id) {
return;
}
navigator.serviceWorker.removeEventListener('message', onMessage);
if (d.ok) {
resolve(d.data);
} else {
reject(new Error(d.error || 'Unknown error'));
}
};
navigator.serviceWorker.addEventListener('message', onMessage);
controller.postMessage({ id, type, payload });
});
}

async function loadCsv(text) {
setError('');
const data = await sendMessage('LOAD_CSV', { csvText: text });
renderTable(data);
}

async function runQuery(sql) {
setError('');
const data = await sendMessage('QUERY', { sql });
renderTable(data);
}

async function resetView() {
setError('');
const data = await sendMessage('RESET', {});
renderTable(data);
}

async function initSw() {
if (!('serviceWorker' in navigator)) {
setSwStatus('Service workers are not available in this browser.');
return false;
}
if (window.location.protocol === 'file:') {
setSwStatus('Open this page over http://localhost (not file://) so the service worker can register.');
return false;
}

try {
await navigator.serviceWorker.register(swUrl.href, { scope: scopeUrl });
} catch (e) {
setSwStatus(`Registration failed: ${e.message || e}`);
return false;
}

if (!navigator.serviceWorker.controller) {
if (!sessionStorage.getItem('csv-lab-sw-activated')) {
sessionStorage.setItem('csv-lab-sw-activated', '1');
setSwStatus('Activating worker… reloading once.');
window.location.reload();
return false;
}
setSwStatus('No controlling service worker. Try a hard refresh.');
return false;
}

setSwStatus('Service worker active — CSV parsing and queries run in the worker.');
return true;
}

els.file.addEventListener('change', async () => {
const f = els.file.files && els.file.files[0];
if (!f) {
return;
}
if (!navigator.serviceWorker.controller) {
setError('Service worker not ready.');
return;
}
const text = await f.text();
try {
await loadCsv(text);
} catch (e) {
setError(e.message || String(e));
}
});

els.prompt.addEventListener('keydown', async (ev) => {
if (ev.key !== 'Enter') {
return;
}
ev.preventDefault();
if (!navigator.serviceWorker.controller) {
setError('Service worker not ready.');
return;
}
const raw = els.prompt.value.trim();
try {
if (!raw || /^reset$/i.test(raw)) {
await resetView();
if (/^reset$/i.test(raw)) {
els.prompt.value = '';
}
return;
}
await runQuery(raw);
} catch (e) {
setError(e.message || String(e));
}
});

initSw().then((ok) => {
els.file.disabled = !ok;
if (ok) {
els.tableWrap.innerHTML =
'<p class="muted">Choose a .csv file. Then use the prompt, e.g. <code>SELECT DISTINCT(JOBS) WHERE location = \'US\'</code></p>';
}
});
})();
168 changes: 168 additions & 0 deletions web/csv-lab/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSV Lab — in-browser worker SQL</title>
<style>
:root {
--bg: #07111f;
--card: rgba(14, 27, 46, 0.9);
--border: rgba(67, 224, 255, 0.22);
--cyan: #43e0ff;
--text: #eaf6ff;
--muted: #97afc6;
--error: #ff8b8b;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
background: linear-gradient(180deg, var(--bg), #050b14);
color: var(--text);
padding: 32px 20px 48px;
}
.wrap {
max-width: 1100px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 8px;
}
.sub {
color: var(--muted);
font-size: 0.95rem;
margin-bottom: 24px;
line-height: 1.5;
}
.sub code { color: var(--cyan); font-size: 0.88em; }
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
}
label.file-label {
display: inline-block;
cursor: pointer;
padding: 10px 16px;
border-radius: 10px;
border: 1px solid var(--border);
color: var(--cyan);
font-weight: 600;
margin-bottom: 12px;
}
#csvFile { display: none; }
#meta {
font-size: 0.85rem;
color: var(--muted);
margin-bottom: 10px;
}
#tableWrap {
max-height: min(520px, 60vh);
overflow: auto;
border-radius: 10px;
border: 1px solid rgba(98, 168, 255, 0.15);
background: rgba(5, 12, 22, 0.6);
}
table.dataframe {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
font-family: var(--mono);
}
table.dataframe th,
table.dataframe td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid rgba(98, 168, 255, 0.12);
}
table.dataframe th {
position: sticky;
top: 0;
background: rgba(10, 23, 39, 0.95);
color: var(--cyan);
font-weight: 600;
}
.prompt-row {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--mono);
font-size: 0.9rem;
}
.prompt-row label {
color: var(--cyan);
flex-shrink: 0;
}
#sqlPrompt {
flex: 1;
background: rgba(5, 12, 22, 0.85);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
padding: 10px 12px;
font: inherit;
}
#sqlPrompt:focus {
outline: 2px solid rgba(67, 224, 255, 0.35);
outline-offset: 2px;
}
#sqlError {
margin-top: 10px;
color: var(--error);
font-family: var(--mono);
font-size: 0.85rem;
white-space: pre-wrap;
}
#sqlError[hidden] { display: none; }
#swStatus {
margin-top: 16px;
font-size: 0.8rem;
color: var(--muted);
}
.muted { color: var(--muted); font-size: 0.9rem; }
a.back {
color: var(--cyan);
text-decoration: none;
font-size: 0.9rem;
}
a.back:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="../index.html">← MeWannaJob home</a>
<h1>CSV Lab</h1>
<p class="sub">
Load a CSV: parsing and queries run in a <strong>scoped service worker</strong> (not on a server).
Micro-SQL: <code>SELECT *</code>, <code>SELECT col1, col2</code>,
<code>SELECT DISTINCT col</code> or <code>SELECT DISTINCT (col)</code>,
optional <code>WHERE col = 'value' AND …</code>. Type <code>RESET</code> or leave empty + Enter to show the loaded preview again.
</p>

<div class="panel">
<label class="file-label" for="csvFile">Choose .csv file</label>
<input type="file" id="csvFile" accept=".csv,text/csv" />
<div id="meta"></div>
<div id="tableWrap"></div>
</div>

<div class="panel">
<div class="prompt-row">
<label for="sqlPrompt">&gt;</label>
<input type="text" id="sqlPrompt" placeholder="SELECT DISTINCT(JOBS) WHERE location = 'US'" autocomplete="off" spellcheck="false" />
</div>
<div id="sqlError" hidden></div>
</div>

<p id="swStatus"></p>
</div>
<script src="csv-lab-client.js"></script>
</body>
</html>
Loading