broadfield-dev commited on
Commit
5ee4c2d
·
verified ·
1 Parent(s): 97353a3

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +232 -187
templates/index.html CHANGED
@@ -2,187 +2,171 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>HF Dataset ETL Command Center</title>
6
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
7
  <style>
8
- .json-badge { background-color: #ffc107; color: #000; font-size: 0.75em; padding: 2px 6px; border-radius: 4px; font-weight: bold; }
9
- .box { border: 1px solid #e0e0e0; padding: 25px; border-radius: 12px; margin-bottom: 25px; background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
10
- .step-num { font-weight: bold; color: #6c757d; margin-bottom: 10px; display: block; }
11
- pre { background: #f8f9fa; padding: 15px; max-height: 300px; overflow: auto; border-radius: 6px; border: 1px solid #eee; }
12
- .form-label { font-weight: 500; font-size: 0.9em; }
13
- .table-hover tbody tr:hover { background-color: #f8f9fa; }
 
 
 
 
 
 
14
  </style>
15
  </head>
16
- <body class="bg-light">
17
 
18
- <div class="container mt-4 mb-5">
19
- <div class="d-flex align-items-center mb-4">
20
- <h2 class="me-3">🛠️ Hugging Face Dataset Command Center</h2>
 
 
 
21
  </div>
22
-
23
- <!-- Step 1: Configuration -->
24
  <div class="box">
25
- <span class="step-num">STEP 1: SOURCE SELECTION</span>
26
- <div class="row g-3">
27
- <div class="col-md-3">
28
- <label class="form-label">HF Write Token</label>
29
- <input type="password" id="token" class="form-control" placeholder="hf_...">
30
- </div>
31
  <div class="col-md-5">
32
- <label class="form-label">Dataset Repository ID</label>
33
  <div class="input-group">
34
- <input type="text" id="dataset_id" class="form-control" placeholder="e.g. the_stack">
35
- <button class="btn btn-primary" onclick="analyzeMetadata()">Analyze Dataset</button>
36
  </div>
37
  </div>
38
  <div class="col-md-2">
39
- <label class="form-label">Config (Subset)</label>
40
- <select id="config_select" class="form-select" onchange="updateSplits()" disabled>
41
- <option>Select...</option>
42
- </select>
43
  </div>
44
  <div class="col-md-2">
45
- <label class="form-label">Split</label>
46
- <select id="split_select" class="form-select" disabled>
47
- <option>Select...</option>
48
- </select>
49
  </div>
50
- </div>
51
- <div class="mt-3 text-end">
52
- <button class="btn btn-outline-dark" id="load_sample_btn" onclick="inspectRows()" disabled>Load Sample Data ⬇</button>
53
  </div>
54
  </div>
55
 
56
- <!-- Step 2: Inspector -->
57
- <div id="inspector-panel" class="box" style="display:none;">
58
- <span class="step-num">STEP 2: SCHEMA & RECIPE</span>
59
 
60
- <div class="row">
61
- <div class="col-md-6">
62
- <h5>Source Columns</h5>
63
- <p class="text-muted small">We detected the following columns. Use the tools on the right to modify them.</p>
64
- <table class="table table-sm table-hover" id="col-table">
65
- <thead><tr><th>Name</th><th>Type</th></tr></thead>
66
- <tbody id="col-list"></tbody>
67
- </table>
68
- </div>
69
-
70
- <div class="col-md-6 border-start">
71
- <h5 class="mb-3">Transformation Recipe</h5>
72
 
 
73
  <div class="mb-3">
74
- <label class="form-label">1. Extract from JSON</label>
75
- <input type="text" id="json-rule" class="form-control mb-1" placeholder="col_name: key1, nested.key2">
76
- <small class="text-muted">Format: <code>source_col: new_key1, meta.id</code></small>
77
  </div>
78
 
79
- <div class="mb-3">
80
- <label class="form-label">2. Rename Columns</label>
81
- <input type="text" id="rename-rule" class="form-control" placeholder="old_name=new_name">
 
 
 
82
  </div>
83
 
84
- <div class="mb-3">
85
- <label class="form-label">3. Drop Columns</label>
86
- <input type="text" id="drop-rule" class="form-control" placeholder="col_to_delete">
 
87
  </div>
 
 
 
 
 
 
 
 
88
 
89
- <div class="mb-3">
90
- <label class="form-label">4. Filter Rows (Python Condition)</label>
91
- <input type="text" id="filter-rule" class="form-control" placeholder="len(text) > 500">
92
- <small class="text-muted">Condition applies to <b>transformed</b> data.</small>
93
  </div>
94
 
95
- <button class="btn btn-success w-100 mt-2" onclick="runPreview()">Preview Transformation </button>
96
- </div>
97
- </div>
98
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- <!-- Step 3: Output -->
101
- <div id="preview-panel" class="box" style="display:none;">
102
- <span class="step-num">STEP 3: PREVIEW & PUSH</span>
103
-
104
- <h6>Transformed Preview (First 5 Rows)</h6>
105
- <div id="preview-area"></div>
106
-
107
- <hr>
108
- <div class="row g-3 align-items-end">
109
- <div class="col-md-5">
110
- <label class="form-label">Target Repo ID</label>
111
- <input type="text" id="target_id" class="form-control" placeholder="username/new-dataset-name">
112
- </div>
113
- <div class="col-md-3">
114
- <label class="form-label">Max Rows (Optional)</label>
115
- <input type="number" id="max_rows" class="form-control" placeholder="All">
116
- </div>
117
- <div class="col-md-4">
118
- <button class="btn btn-danger w-100" onclick="executeJob()">🚀 Launch ETL Job</button>
119
  </div>
120
  </div>
121
- <div id="job-status" class="mt-3 p-3 rounded bg-light fw-bold text-center" style="display:none"></div>
122
  </div>
123
  </div>
124
 
125
  <script>
126
- // --- Step 1 Functions ---
127
- async function analyzeMetadata() {
128
- const btn = document.querySelector('button[onclick="analyzeMetadata()"]');
129
- btn.disabled = true; btn.innerText = "Fetching...";
130
-
131
- const payload = {
132
- token: document.getElementById('token').value,
133
- dataset_id: document.getElementById('dataset_id').value
134
- };
135
 
 
 
 
 
136
  try {
137
  const res = await fetch('/analyze_metadata', {
138
  method: 'POST', headers: {'Content-Type': 'application/json'},
139
- body: JSON.stringify(payload)
 
 
 
140
  });
141
  const data = await res.json();
142
-
143
  if(data.status === 'success') {
144
- // Populate Configs
145
- const configSel = document.getElementById('config_select');
146
- configSel.innerHTML = '';
147
- data.configs.forEach(c => {
148
- configSel.innerHTML += `<option value="${c}">${c}</option>`;
149
- });
150
- configSel.disabled = false;
151
-
152
- // Populate Splits (for first config)
153
  populateSplits(data.splits);
154
- document.getElementById('load_sample_btn').disabled = false;
155
- } else {
156
- alert('Error: ' + data.message);
157
- }
158
  } catch(e) { alert(e); }
159
-
160
- btn.disabled = false; btn.innerText = "Analyze Dataset";
161
  }
162
 
163
  function populateSplits(splits) {
164
- const splitSel = document.getElementById('split_select');
165
- splitSel.innerHTML = '';
166
- splits.forEach(s => {
167
- splitSel.innerHTML += `<option value="${s}">${s}</option>`;
168
- });
169
- splitSel.disabled = false;
170
  }
171
 
172
  async function updateSplits() {
173
- const conf = document.getElementById('config_select').value;
174
- const ds = document.getElementById('dataset_id').value;
175
- const res = await fetch('/get_splits', {
176
- method: 'POST', headers: {'Content-Type': 'application/json'},
177
- body: JSON.stringify({dataset_id: ds, config: conf, token: document.getElementById('token').value})
178
- });
179
- const data = await res.json();
180
- if(data.status === 'success') populateSplits(data.splits);
181
  }
182
 
183
- // --- Step 2 Functions ---
184
- async function inspectRows() {
185
- document.getElementById('load_sample_btn').innerText = "Loading...";
 
 
186
  const payload = {
187
  token: document.getElementById('token').value,
188
  dataset_id: document.getElementById('dataset_id').value,
@@ -195,53 +179,121 @@
195
  body: JSON.stringify(payload)
196
  });
197
  const data = await res.json();
198
-
199
- document.getElementById('load_sample_btn').innerText = "Load Sample Data ⬇";
200
 
201
  if(data.status === 'success') {
202
- document.getElementById('inspector-panel').style.display = 'block';
203
- const tbody = document.getElementById('col-list');
204
- tbody.innerHTML = '';
 
 
205
 
206
- for (const [col, info] of Object.entries(data.analysis)) {
207
- let badge = info.is_json_string ? '<span class="json-badge">JSON STR</span>' : '';
208
- tbody.innerHTML += `<tr>
209
- <td><b>${col}</b></td>
210
- <td>${info.type} ${badge}</td>
211
- </tr>`;
212
  }
213
  } else {
214
  alert(data.message);
215
  }
 
216
  }
217
 
218
- // --- Step 3 Functions ---
219
- function getRecipe() {
220
- const jsonRule = document.getElementById('json-rule').value;
221
- const renameRule = document.getElementById('rename-rule').value;
222
- const dropRule = document.getElementById('drop-rule').value;
223
- const filterRule = document.getElementById('filter-rule').value;
 
 
 
 
 
 
 
 
 
 
224
 
225
- let recipe = { json_expansions: [], renames: {}, drops: [], filters: [] };
 
 
 
226
 
227
- if(jsonRule.includes(':')) {
228
- let [col, keys] = jsonRule.split(':');
229
- recipe.json_expansions.push({
230
- col: col.trim(),
231
- keys: keys.split(',').map(k => k.trim())
232
- });
233
- }
234
- if(renameRule.includes('=')) {
235
- let [oldC, newC] = renameRule.split('=');
236
- recipe.renames[oldC.trim()] = newC.trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
238
- if(dropRule) recipe.drops.push(dropRule.trim());
239
- if(filterRule) recipe.filters.push(filterRule.trim());
240
 
241
- return recipe;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  }
243
 
244
  async function runPreview() {
 
 
 
245
  const payload = {
246
  token: document.getElementById('token').value,
247
  dataset_id: document.getElementById('dataset_id').value,
@@ -256,12 +308,10 @@
256
  });
257
  const data = await res.json();
258
 
259
- document.getElementById('preview-panel').style.display = 'block';
260
- const area = document.getElementById('preview-area');
261
- if (data.status === 'success') {
262
- area.innerHTML = `<pre>${JSON.stringify(data.rows, null, 2)}</pre>`;
263
  } else {
264
- area.innerHTML = `<div class="text-danger">${data.message}</div>`;
265
  }
266
  }
267
 
@@ -283,31 +333,26 @@
283
  const data = await res.json();
284
 
285
  if(data.status === 'started') {
286
- document.getElementById('job-status').style.display = 'block';
287
- pollStatus(data.job_id);
288
- }
289
- }
290
-
291
- function pollStatus(jobId) {
292
- const statusDiv = document.getElementById('job-status');
293
- const interval = setInterval(async () => {
294
- const res = await fetch(`/status/${jobId}`);
295
- const data = await res.json();
296
 
297
- if(data.status === 'running') {
298
- statusDiv.innerText = "Processing... (Checking every 2s)";
299
- statusDiv.className = "mt-3 p-3 rounded bg-info text-white fw-bold text-center";
300
- } else if (data.status === 'completed') {
301
- statusDiv.innerText = `Success! Pushed ${data.result.rows_processed} rows to Hub.`;
302
- statusDiv.className = "mt-3 p-3 rounded bg-success text-white fw-bold text-center";
303
- clearInterval(interval);
304
- } else if (data.status === 'failed') {
305
- statusDiv.innerText = `Error: ${data.error}`;
306
- statusDiv.className = "mt-3 p-3 rounded bg-danger text-white fw-bold text-center";
307
- clearInterval(interval);
308
- }
309
- }, 2000);
310
  }
311
  </script>
 
312
  </body>
313
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>HF Dataset Builder</title>
6
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
7
  <style>
8
+ body { background-color: #f4f6f8; }
9
+ .box { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; margin-bottom: 20px; }
10
+ .source-col-row { cursor: pointer; transition: background 0.1s; }
11
+ .source-col-row:hover { background-color: #e9ecef; }
12
+ .source-col-row.active { background-color: #cfe2ff; font-weight: bold; }
13
+ pre { background: #212529; color: #adbac7; padding: 15px; border-radius: 6px; font-size: 0.85em; max-height: 250px; overflow: auto; }
14
+ .step-header { font-size: 0.8em; text-transform: uppercase; letter-spacing: 1px; color: #6c757d; font-weight: 700; margin-bottom: 10px; }
15
+
16
+ /* Builder Styles */
17
+ .builder-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; padding: 10px; border: 1px solid #eee; border-radius: 6px; background: #fff; }
18
+ .builder-row:hover { border-color: #cbd5e1; box-shadow: 0 2px 4px rgba(0,0,0,0.03); }
19
+ .drag-handle { cursor: grab; color: #adb5bd; padding: 0 5px; }
20
  </style>
21
  </head>
22
+ <body>
23
 
24
+ <div class="container-fluid p-4">
25
+ <div class="d-flex justify-content-between align-items-center mb-4">
26
+ <h3>🏗️ Hugging Face Dataset Architect</h3>
27
+ <div class="d-flex gap-2">
28
+ <input type="password" id="token" class="form-control form-control-sm" placeholder="HF Write Token" style="width: 200px;">
29
+ </div>
30
  </div>
31
+
32
+ <!-- 1. SOURCE CONFIG -->
33
  <div class="box">
34
+ <div class="step-header">1. Connect Data Source</div>
35
+ <div class="row g-2">
 
 
 
 
36
  <div class="col-md-5">
 
37
  <div class="input-group">
38
+ <input type="text" id="dataset_id" class="form-control" placeholder="Source Repo ID (e.g. the_stack)">
39
+ <button class="btn btn-primary" onclick="loadMetadata()">Connect</button>
40
  </div>
41
  </div>
42
  <div class="col-md-2">
43
+ <select id="config_select" class="form-select" onchange="updateSplits()" disabled><option>Config...</option></select>
 
 
 
44
  </div>
45
  <div class="col-md-2">
46
+ <select id="split_select" class="form-select" disabled><option>Split...</option></select>
47
+ </div>
48
+ <div class="col-md-3">
49
+ <button id="inspect_btn" class="btn btn-success w-100" onclick="startInspection()" disabled>Analyze Schema</button>
50
  </div>
 
 
 
51
  </div>
52
  </div>
53
 
54
+ <!-- MAIN WORKSPACE -->
55
+ <div class="row" id="workspace" style="display:none;">
 
56
 
57
+ <!-- LEFT: SOURCE EXPLORER -->
58
+ <div class="col-md-4">
59
+ <div class="box h-100">
60
+ <div class="step-header">2. Source Explorer</div>
 
 
 
 
 
 
 
 
61
 
62
+ <!-- Filter Input -->
63
  <div class="mb-3">
64
+ <label class="form-label small fw-bold">Source Filter (Python)</label>
65
+ <input type="text" id="source_filter" class="form-control form-control-sm" placeholder="e.g. len(text) > 100">
 
66
  </div>
67
 
68
+ <!-- Column List -->
69
+ <div class="card mb-3">
70
+ <div class="card-header py-1 small fw-bold bg-light">Available Columns</div>
71
+ <ul class="list-group list-group-flush small" id="source_col_list" style="max-height: 300px; overflow-y: auto;">
72
+ <!-- Populated by JS -->
73
+ </ul>
74
  </div>
75
 
76
+ <!-- Preview Box -->
77
+ <div class="mt-3">
78
+ <label class="form-label small fw-bold">Column Data Preview (5 rows)</label>
79
+ <div id="data_preview_box"><pre class="text-muted">Click a column above to see data.</pre></div>
80
  </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- RIGHT: COLUMN BUILDER -->
85
+ <div class="col-md-8">
86
+ <div class="box h-100">
87
+ <div class="step-header">3. New Dataset Schema</div>
88
+ <p class="small text-muted">Construct your new dataset structure. Select source fields (including nested JSON keys) for each new column.</p>
89
 
90
+ <div id="builder_container">
91
+ <!-- Builder Rows go here -->
 
 
92
  </div>
93
 
94
+ <button class="btn btn-sm btn-outline-primary mt-2" onclick="addBuilderRow()">+ Add Column</button>
95
+
96
+ <hr class="my-4">
97
+
98
+ <div class="step-header">4. Publish</div>
99
+ <div class="row g-2 align-items-end">
100
+ <div class="col-md-5">
101
+ <label class="small fw-bold">Target Repository</label>
102
+ <input type="text" id="target_id" class="form-control" placeholder="username/new-dataset">
103
+ </div>
104
+ <div class="col-md-2">
105
+ <label class="small fw-bold">Max Rows</label>
106
+ <input type="number" id="max_rows" class="form-control" placeholder="All">
107
+ </div>
108
+ <div class="col-md-5 d-flex gap-2">
109
+ <button class="btn btn-outline-dark w-50" onclick="runPreview()">Preview 5 Rows</button>
110
+ <button class="btn btn-danger w-50" onclick="executeJob()">🚀 Push to Hub</button>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Status & Preview Output -->
115
+ <div id="job_status" class="alert mt-3" style="display:none;"></div>
116
+ <div id="final_preview" class="mt-3" style="display:none;"></div>
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
  </div>
 
120
  </div>
121
  </div>
122
 
123
  <script>
124
+ // Global State
125
+ let schemaTree = {};
126
+ let sampleRows = [];
 
 
 
 
 
 
127
 
128
+ // --- 1. CONNECTION ---
129
+ async function loadMetadata() {
130
+ const btn = document.querySelector('button[onclick="loadMetadata()"]');
131
+ btn.disabled = true; btn.innerText = "...";
132
  try {
133
  const res = await fetch('/analyze_metadata', {
134
  method: 'POST', headers: {'Content-Type': 'application/json'},
135
+ body: JSON.stringify({
136
+ token: document.getElementById('token').value,
137
+ dataset_id: document.getElementById('dataset_id').value
138
+ })
139
  });
140
  const data = await res.json();
 
141
  if(data.status === 'success') {
142
+ const confSel = document.getElementById('config_select');
143
+ confSel.innerHTML = '';
144
+ data.configs.forEach(c => confSel.innerHTML += `<option value="${c}">${c}</option>`);
145
+ confSel.disabled = false;
146
+
 
 
 
 
147
  populateSplits(data.splits);
148
+ document.getElementById('inspect_btn').disabled = false;
149
+ } else { alert(data.message); }
 
 
150
  } catch(e) { alert(e); }
151
+ btn.disabled = false; btn.innerText = "Connect";
 
152
  }
153
 
154
  function populateSplits(splits) {
155
+ const sel = document.getElementById('split_select');
156
+ sel.innerHTML = '';
157
+ splits.forEach(s => sel.innerHTML += `<option value="${s}">${s}</option>`);
158
+ sel.disabled = false;
 
 
159
  }
160
 
161
  async function updateSplits() {
162
+ // ... (Similar logic as previous version) ...
 
 
 
 
 
 
 
163
  }
164
 
165
+ // --- 2. INSPECTION ---
166
+ async function startInspection() {
167
+ const btn = document.getElementById('inspect_btn');
168
+ btn.innerText = "Scanning JSON..."; btn.disabled = true;
169
+
170
  const payload = {
171
  token: document.getElementById('token').value,
172
  dataset_id: document.getElementById('dataset_id').value,
 
179
  body: JSON.stringify(payload)
180
  });
181
  const data = await res.json();
 
 
182
 
183
  if(data.status === 'success') {
184
+ document.getElementById('workspace').style.display = 'flex';
185
+ sampleRows = data.samples;
186
+ schemaTree = data.schema_tree;
187
+
188
+ renderSourceList();
189
 
190
+ // Auto-add first few columns to builder as default
191
+ document.getElementById('builder_container').innerHTML = '';
192
+ let count = 0;
193
+ for(let root in schemaTree) {
194
+ if(count < 3) addBuilderRow(root, root);
195
+ count++;
196
  }
197
  } else {
198
  alert(data.message);
199
  }
200
+ btn.innerText = "Analyze Schema"; btn.disabled = false;
201
  }
202
 
203
+ // --- 3. UI RENDERING ---
204
+ function renderSourceList() {
205
+ const list = document.getElementById('source_col_list');
206
+ list.innerHTML = '';
207
+
208
+ for (const [root, paths] of Object.entries(schemaTree)) {
209
+ let isJson = paths.length > 1; // If it has children, it was parsed as JSON/Object
210
+ let badge = isJson ? '<span class="badge bg-warning text-dark float-end" style="font-size:0.7em">JSON</span>' : '';
211
+
212
+ let li = document.createElement('li');
213
+ li.className = "list-group-item source-col-row d-flex justify-content-between align-items-center";
214
+ li.innerHTML = `<span>${root}</span> ${badge}`;
215
+ li.onclick = () => previewColumn(root, li);
216
+ list.appendChild(li);
217
+ }
218
+ }
219
 
220
+ function previewColumn(colName, el) {
221
+ // Highlight active
222
+ document.querySelectorAll('.source-col-row').forEach(e => e.classList.remove('active'));
223
+ el.classList.add('active');
224
 
225
+ // Show data
226
+ const box = document.getElementById('data_preview_box');
227
+ let html = `<small class="text-muted d-block mb-2">Values for <b>${colName}</b>:</small><pre>`;
228
+
229
+ sampleRows.forEach(row => {
230
+ let val = row[colName];
231
+ if(typeof val === 'object') val = JSON.stringify(val, null, 2);
232
+ html += `${val}\n`;
233
+ });
234
+ html += `</pre>`;
235
+ box.innerHTML = html;
236
+ }
237
+
238
+ function addBuilderRow(targetName='', sourcePath='') {
239
+ const container = document.getElementById('builder_container');
240
+ const id = Date.now();
241
+
242
+ // Build Options for Select
243
+ let options = '';
244
+ for (const [root, paths] of Object.entries(schemaTree)) {
245
+ if (paths.length === 1) {
246
+ // Simple column
247
+ options += `<option value="${paths[0]}" ${paths[0]===sourcePath?'selected':''}>${paths[0]}</option>`;
248
+ } else {
249
+ // Nested group
250
+ options += `<optgroup label="${root}">`;
251
+ paths.forEach(p => {
252
+ options += `<option value="${p}" ${p===sourcePath?'selected':''}>${p}</option>`;
253
+ });
254
+ options += `</optgroup>`;
255
+ }
256
  }
 
 
257
 
258
+ const div = document.createElement('div');
259
+ div.className = "builder-row";
260
+ div.innerHTML = `
261
+ <span class="drag-handle">☰</span>
262
+ <div style="flex:1">
263
+ <label class="form-label small mb-0 text-muted">Target Name</label>
264
+ <input type="text" class="form-control form-control-sm target-name" value="${targetName}" placeholder="new_column_name">
265
+ </div>
266
+ <div style="flex:0 0 20px; text-align:center;">←</div>
267
+ <div style="flex:1">
268
+ <label class="form-label small mb-0 text-muted">Source Field</label>
269
+ <select class="form-select form-select-sm source-path">${options}</select>
270
+ </div>
271
+ <button class="btn btn-sm text-danger" onclick="this.parentElement.remove()">×</button>
272
+ `;
273
+ container.appendChild(div);
274
+ }
275
+
276
+ // --- 4. EXECUTION ---
277
+ function getRecipe() {
278
+ const rows = document.querySelectorAll('.builder-row');
279
+ let columns = [];
280
+ rows.forEach(r => {
281
+ columns.push({
282
+ name: r.querySelector('.target-name').value,
283
+ source: r.querySelector('.source-path').value
284
+ });
285
+ });
286
+
287
+ return {
288
+ filter_rule: document.getElementById('source_filter').value,
289
+ columns: columns
290
+ };
291
  }
292
 
293
  async function runPreview() {
294
+ const box = document.getElementById('final_preview');
295
+ box.style.display = 'block'; box.innerHTML = 'Generating preview...';
296
+
297
  const payload = {
298
  token: document.getElementById('token').value,
299
  dataset_id: document.getElementById('dataset_id').value,
 
308
  });
309
  const data = await res.json();
310
 
311
+ if(data.status === 'success') {
312
+ box.innerHTML = `<pre>${JSON.stringify(data.rows, null, 2)}</pre>`;
 
 
313
  } else {
314
+ box.innerHTML = `<div class="text-danger">${data.message}</div>`;
315
  }
316
  }
317
 
 
333
  const data = await res.json();
334
 
335
  if(data.status === 'started') {
336
+ const statusDiv = document.getElementById('job_status');
337
+ statusDiv.style.display = 'block';
338
+ statusDiv.className = 'alert alert-info';
339
+ statusDiv.innerText = "Job started...";
 
 
 
 
 
 
340
 
341
+ const interval = setInterval(async () => {
342
+ const s = await fetch(`/status/${data.job_id}`).then(r=>r.json());
343
+ if(s.status === 'completed') {
344
+ statusDiv.className = 'alert alert-success';
345
+ statusDiv.innerText = `Done! Processed ${s.result.rows_processed} rows.`;
346
+ clearInterval(interval);
347
+ } else if (s.status === 'failed') {
348
+ statusDiv.className = 'alert alert-danger';
349
+ statusDiv.innerText = `Error: ${s.error}`;
350
+ clearInterval(interval);
351
+ }
352
+ }, 2000);
353
+ }
354
  }
355
  </script>
356
+
357
  </body>
358
  </html>