broadfield-dev commited on
Commit
f37234a
·
verified ·
1 Parent(s): 4425935

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +245 -248
templates/index.html CHANGED
@@ -2,21 +2,25 @@
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>
@@ -24,111 +28,216 @@
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'},
@@ -139,220 +248,108 @@
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,
173
- config: document.getElementById('config_select').value,
174
- split: document.getElementById('split_select').value
175
- };
176
 
177
  const res = await fetch('/inspect_rows', {
178
  method: 'POST', headers: {'Content-Type': 'application/json'},
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,
300
- config: document.getElementById('config_select').value,
301
- split: document.getElementById('split_select').value,
302
- recipe: getRecipe()
303
- };
304
-
305
  const res = await fetch('/preview', {
306
  method: 'POST', headers: {'Content-Type': 'application/json'},
307
- body: JSON.stringify(payload)
 
 
 
 
 
 
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
 
318
  async function executeJob() {
319
- const payload = {
320
- token: document.getElementById('token').value,
321
- dataset_id: document.getElementById('dataset_id').value,
322
- config: document.getElementById('config_select').value,
323
- split: document.getElementById('split_select').value,
324
- target_id: document.getElementById('target_id').value,
325
- recipe: getRecipe(),
326
- max_rows: document.getElementById('max_rows').value
327
- };
328
-
329
  const res = await fetch('/execute', {
330
  method: 'POST', headers: {'Content-Type': 'application/json'},
331
- body: JSON.stringify(payload)
 
 
 
 
 
 
 
 
332
  });
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>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>Dataset Architect</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; font-size: 0.9rem; }
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; }
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.8em; max-height: 250px; overflow: auto; }
 
14
 
15
  /* Builder Styles */
16
+ .builder-row { border: 1px solid #dee2e6; border-radius: 6px; background: #fff; padding: 10px; margin-bottom: 10px; position: relative; }
17
+ .builder-row.mode-list { border-left: 4px solid #198754; }
18
+ .builder-row.mode-python { border-left: 4px solid #fd7e14; }
19
+ .builder-row.mode-simple { border-left: 4px solid #0d6efd; }
20
+
21
+ .mode-badge { position: absolute; top: -10px; right: 10px; font-size: 0.7em; padding: 2px 6px; background: #fff; border: 1px solid #ccc; border-radius: 4px; text-transform: uppercase; font-weight: bold; color: #666; }
22
+
23
+ .logic-group { background: #f8f9fa; padding: 8px; border-radius: 4px; margin-top: 5px; border: 1px dashed #ccc; }
24
  </style>
25
  </head>
26
  <body>
 
28
  <div class="container-fluid p-4">
29
  <div class="d-flex justify-content-between align-items-center mb-4">
30
  <h3>🏗️ Hugging Face Dataset Architect</h3>
31
+ <div><input type="password" id="token" class="form-control form-control-sm" placeholder="HF Token" style="width: 200px;"></div>
 
 
32
  </div>
33
 
34
+ <!-- 1. CONFIG -->
35
  <div class="box">
 
36
  <div class="row g-2">
37
+ <div class="col-md-4">
38
  <div class="input-group">
39
+ <input type="text" id="dataset_id" class="form-control" placeholder="Source ID (e.g. the_stack)">
40
  <button class="btn btn-primary" onclick="loadMetadata()">Connect</button>
41
  </div>
42
  </div>
43
+ <div class="col-md-2"><select id="config_select" class="form-select" onchange="updateSplits()" disabled><option>Config...</option></select></div>
44
+ <div class="col-md-2"><select id="split_select" class="form-select" disabled><option>Split...</option></select></div>
45
+ <div class="col-md-4"><button id="inspect_btn" class="btn btn-success w-100" onclick="startInspection()" disabled>Analyze Schema</button></div>
 
 
 
 
 
 
46
  </div>
47
  </div>
48
 
49
+ <!-- WORKSPACE -->
50
  <div class="row" id="workspace" style="display:none;">
51
+ <!-- LEFT: EXPLORER -->
 
52
  <div class="col-md-4">
53
  <div class="box h-100">
54
+ <h6>Source Columns</h6>
55
+ <div class="mb-3"><input type="text" id="source_filter" class="form-control form-control-sm" placeholder="Filter rows: len(text) > 100"></div>
56
+ <ul class="list-group list-group-flush mb-3" id="source_col_list" style="max-height: 300px; overflow-y: auto;"></ul>
57
+ <h6>Data Preview</h6>
58
+ <div id="data_preview_box"><pre class="text-muted">Select a column.</pre></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  </div>
60
  </div>
61
 
62
+ <!-- RIGHT: BUILDER -->
63
  <div class="col-md-8">
64
  <div class="box h-100">
65
+ <div class="d-flex justify-content-between mb-3">
66
+ <h6>New Schema Definition</h6>
67
+ <button class="btn btn-sm btn-outline-primary" onclick="addBuilderRow()">+ Add Column</button>
 
 
68
  </div>
 
 
 
 
69
 
70
+ <div id="builder_container"></div>
71
+
72
+ <hr>
73
  <div class="row g-2 align-items-end">
74
+ <div class="col-md-4"><input type="text" id="target_id" class="form-control" placeholder="target/repo"></div>
75
+ <div class="col-md-2"><input type="number" id="max_rows" class="form-control" placeholder="Max Rows"></div>
76
+ <div class="col-md-6 d-flex gap-2">
77
+ <button class="btn btn-secondary w-50" onclick="runPreview()">Preview</button>
78
+ <button class="btn btn-danger w-50" onclick="executeJob()">🚀 Push</button>
 
 
 
 
 
 
79
  </div>
80
  </div>
81
 
 
82
  <div id="job_status" class="alert mt-3" style="display:none;"></div>
83
  <div id="final_preview" class="mt-3" style="display:none;"></div>
 
84
  </div>
85
  </div>
86
  </div>
87
  </div>
88
 
89
  <script>
90
+ let schemaData = {};
 
91
  let sampleRows = [];
92
 
93
+ // --- TEMPLATES ---
94
+ function addBuilderRow() {
95
+ const container = document.getElementById('builder_container');
96
+ const id = 'row_' + Date.now();
97
+
98
+ // Build Source Options
99
+ let sourceOpts = '<option value="">Select Source...</option>';
100
+ for(let col in schemaData) {
101
+ let label = col + (schemaData[col].type === 'List' ? ' [List]' : '');
102
+ sourceOpts += `<option value="${col}">${label}</option>`;
103
+ }
104
+
105
+ const div = document.createElement('div');
106
+ div.className = "builder-row mode-simple";
107
+ div.id = id;
108
+
109
+ div.innerHTML = `
110
+ <span class="mode-badge">Simple Path</span>
111
+ <div class="row g-2 align-items-center mb-2">
112
+ <div class="col-md-3">
113
+ <input type="text" class="form-control form-control-sm target-name" placeholder="New Col Name">
114
+ </div>
115
+ <div class="col-md-3">
116
+ <select class="form-select form-select-sm mode-select" onchange="changeMode('${id}', this.value)">
117
+ <option value="simple">Simple Path</option>
118
+ <option value="list_search">List Filter (Get X where Y=Z)</option>
119
+ <option value="python">Custom Python</option>
120
+ </select>
121
+ </div>
122
+ <div class="col-md-5 text-end">
123
+ <button class="btn btn-sm btn-link text-danger text-decoration-none" onclick="this.closest('.builder-row').remove()">Remove</button>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- DYNAMIC INPUT AREA -->
128
+ <div class="inputs-area">
129
+ <!-- Default Simple Mode -->
130
+ <div class="input-group input-group-sm">
131
+ <span class="input-group-text">Source</span>
132
+ <select class="form-select source-col">${sourceOpts}</select>
133
+ <input type="text" class="form-control" placeholder="Optional .dot.notation" id="${id}_simple_extra">
134
+ </div>
135
+ </div>
136
+ `;
137
+ container.appendChild(div);
138
+ }
139
+
140
+ function changeMode(rowId, mode) {
141
+ const row = document.getElementById(rowId);
142
+ const area = row.querySelector('.inputs-area');
143
+ const badge = row.querySelector('.mode-badge');
144
+
145
+ // Reset classes
146
+ row.className = `builder-row mode-${mode}`;
147
+
148
+ // Build Source Options again
149
+ let sourceOpts = '<option value="">Select Col...</option>';
150
+ for(let col in schemaData) {
151
+ sourceOpts += `<option value="${col}">${col}</option>`;
152
+ }
153
+
154
+ if(mode === 'simple') {
155
+ badge.innerText = "Simple Path";
156
+ area.innerHTML = `
157
+ <div class="input-group input-group-sm">
158
+ <span class="input-group-text">Source</span>
159
+ <select class="form-select source-col">${sourceOpts}</select>
160
+ <input type="text" class="form-control source-extra" placeholder="Optional .dot.notation (e.g. meta.url)">
161
+ </div>`;
162
+ }
163
+ else if (mode === 'list_search') {
164
+ badge.innerText = "List Logic";
165
+ area.innerHTML = `
166
+ <div class="logic-group">
167
+ <div class="d-flex gap-2 align-items-center mb-1">
168
+ <span class="fw-bold small">FROM</span>
169
+ <select class="form-select form-select-sm source-col" style="width:150px">${sourceOpts}</select>
170
+ <span class="fw-bold small">FIND ITEM WHERE</span>
171
+ </div>
172
+ <div class="d-flex gap-2 align-items-center mb-1">
173
+ <input type="text" class="form-control form-control-sm filter-key" placeholder="Key (e.g. role)">
174
+ <span class="fw-bold small">=</span>
175
+ <input type="text" class="form-control form-control-sm filter-val" placeholder="Value (e.g. user)">
176
+ </div>
177
+ <div class="d-flex gap-2 align-items-center">
178
+ <span class="fw-bold small">EXTRACT</span>
179
+ <input type="text" class="form-control form-control-sm target-key" placeholder="Key (e.g. content or content.analysis)">
180
+ </div>
181
+ </div>`;
182
+ }
183
+ else if (mode === 'python') {
184
+ badge.innerText = "Python";
185
+ area.innerHTML = `
186
+ <div class="input-group input-group-sm">
187
+ <span class="input-group-text">val = </span>
188
+ <input type="text" class="form-control python-expr" placeholder="row['text'].upper() or json.loads(row['meta'])['id']">
189
+ </div>`;
190
+ }
191
+ }
192
+
193
+ function getRecipe() {
194
+ const rows = document.querySelectorAll('.builder-row');
195
+ let columns = [];
196
+
197
+ rows.forEach(r => {
198
+ const name = r.querySelector('.target-name').value;
199
+ const mode = r.querySelector('.mode-select').value;
200
+
201
+ if(!name) return; // skip empty names
202
+
203
+ if(mode === 'simple') {
204
+ let src = r.querySelector('.source-col').value;
205
+ let extra = r.querySelector('.source-extra').value;
206
+ if(extra) { src = extra.startsWith('.') ? src + extra : src + '.' + extra; }
207
+
208
+ columns.push({ type: 'simple', name: name, source: src });
209
+ }
210
+ else if(mode === 'list_search') {
211
+ columns.push({
212
+ type: 'list_search',
213
+ name: name,
214
+ source: r.querySelector('.source-col').value,
215
+ filter_key: r.querySelector('.filter-key').value,
216
+ filter_val: r.querySelector('.filter-val').value,
217
+ target_key: r.querySelector('.target-key').value
218
+ });
219
+ }
220
+ else if(mode === 'python') {
221
+ columns.push({
222
+ type: 'python',
223
+ name: name,
224
+ expression: r.querySelector('.python-expr').value
225
+ });
226
+ }
227
+ });
228
+
229
+ return {
230
+ filter_rule: document.getElementById('source_filter').value,
231
+ columns: columns
232
+ };
233
+ }
234
+
235
+ // --- STANDARD API CALLS (Connect, Inspect, Execute) ---
236
+ // (Same as previous version, just ensure they call getRecipe())
237
+
238
+ async function loadMetadata() { /* Same as before */
239
  const btn = document.querySelector('button[onclick="loadMetadata()"]');
240
+ btn.disabled = true;
241
  try {
242
  const res = await fetch('/analyze_metadata', {
243
  method: 'POST', headers: {'Content-Type': 'application/json'},
 
248
  });
249
  const data = await res.json();
250
  if(data.status === 'success') {
251
+ // Config Select
252
+ const cSel = document.getElementById('config_select');
253
+ cSel.innerHTML = '';
254
+ data.configs.forEach(c => cSel.innerHTML += `<option value="${c}">${c}</option>`);
255
+ cSel.disabled = false;
256
+
257
+ // Splits
258
+ const sSel = document.getElementById('split_select');
259
+ sSel.innerHTML = '';
260
+ data.splits.forEach(s => sSel.innerHTML += `<option value="${s}">${s}</option>`);
261
+ sSel.disabled = false;
262
 
 
263
  document.getElementById('inspect_btn').disabled = false;
264
  } else { alert(data.message); }
265
  } catch(e) { alert(e); }
266
+ btn.disabled = false;
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  async function startInspection() {
270
  const btn = document.getElementById('inspect_btn');
271
+ btn.innerText = "Scanning..."; btn.disabled = true;
 
 
 
 
 
 
 
272
 
273
  const res = await fetch('/inspect_rows', {
274
  method: 'POST', headers: {'Content-Type': 'application/json'},
275
+ body: JSON.stringify({
276
+ token: document.getElementById('token').value,
277
+ dataset_id: document.getElementById('dataset_id').value,
278
+ config: document.getElementById('config_select').value,
279
+ split: document.getElementById('split_select').value
280
+ })
281
  });
282
  const data = await res.json();
283
+
284
  if(data.status === 'success') {
285
  document.getElementById('workspace').style.display = 'flex';
286
  sampleRows = data.samples;
287
+ schemaData = data.schema;
288
 
289
+ // Render Source List
290
+ const list = document.getElementById('source_col_list');
291
+ list.innerHTML = '';
292
+ for(let col in schemaData) {
293
+ let badge = schemaData[col].type === 'List' ? '<span class="badge bg-warning text-dark float-end">List</span>' : '';
294
+ list.innerHTML += `<li class="list-group-item source-col-row" onclick="previewCol('${col}')">${col} ${badge}</li>`;
 
 
295
  }
296
+ } else { alert(data.message); }
 
 
297
  btn.innerText = "Analyze Schema"; btn.disabled = false;
298
  }
299
 
300
+ function previewCol(col) {
301
+ let html = '';
302
+ sampleRows.forEach(r => {
303
+ let v = r[col];
304
+ if(typeof v === 'object') v = JSON.stringify(v, null, 2);
305
+ html += v + "\n" + "-".repeat(20) + "\n";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  });
307
+ document.getElementById('data_preview_box').innerHTML = `<pre>${html}</pre>`;
 
 
 
 
308
  }
309
 
310
  async function runPreview() {
311
  const box = document.getElementById('final_preview');
312
+ box.style.display='block'; box.innerHTML = "Generating...";
313
 
 
 
 
 
 
 
 
 
314
  const res = await fetch('/preview', {
315
  method: 'POST', headers: {'Content-Type': 'application/json'},
316
+ body: JSON.stringify({
317
+ token: document.getElementById('token').value,
318
+ dataset_id: document.getElementById('dataset_id').value,
319
+ config: document.getElementById('config_select').value,
320
+ split: document.getElementById('split_select').value,
321
+ recipe: getRecipe()
322
+ })
323
  });
324
  const data = await res.json();
325
+ if(data.status === 'success') box.innerHTML = `<pre>${JSON.stringify(data.rows, null, 2)}</pre>`;
326
+ else box.innerHTML = `<div class="text-danger">${data.message}</div>`;
 
 
 
 
327
  }
328
 
329
  async function executeJob() {
 
 
 
 
 
 
 
 
 
 
330
  const res = await fetch('/execute', {
331
  method: 'POST', headers: {'Content-Type': 'application/json'},
332
+ body: JSON.stringify({
333
+ token: document.getElementById('token').value,
334
+ dataset_id: document.getElementById('dataset_id').value,
335
+ config: document.getElementById('config_select').value,
336
+ split: document.getElementById('split_select').value,
337
+ target_id: document.getElementById('target_id').value,
338
+ recipe: getRecipe(),
339
+ max_rows: document.getElementById('max_rows').value
340
+ })
341
  });
342
  const data = await res.json();
 
343
  if(data.status === 'started') {
344
+ document.getElementById('job_status').style.display = 'block';
345
+ document.getElementById('job_status').innerText = "Job Started...";
346
+ // Simple polling
347
+ setInterval(async () => {
 
 
348
  const s = await fetch(`/status/${data.job_id}`).then(r=>r.json());
349
+ if(s.status === 'completed') document.getElementById('job_status').innerText = `Done! Processed ${s.result.rows_processed}`;
 
 
 
 
 
 
 
 
350
  }, 2000);
351
  }
352
  }
353
  </script>
 
354
  </body>
355
  </html>