hmb HF Staff commited on
Commit
4e41eff
·
1 Parent(s): 4fced55
Files changed (2) hide show
  1. src/App.svelte +195 -65
  2. src/app.css +0 -3
src/App.svelte CHANGED
@@ -12,6 +12,13 @@
12
  let inputValue: { data: string[][]; headers: string[] } = { data: [[]], headers: [] };
13
  let cleanedValue: { data: string[][]; headers: string[] } = { data: [[]], headers: [] };
14
 
 
 
 
 
 
 
 
15
 
16
  function parseCSVorTSV(text: string) {
17
  if (!text) return { data: [[]], headers: [] };
@@ -20,7 +27,35 @@
20
  const sep = lines[0].includes('\t') ? '\t' : ',';
21
  const headers = lines[0].split(sep).map(h => h.trim());
22
  const data = lines.slice(1).map(line => line.split(sep).map(cell => cell.trim()));
23
- return { data, headers };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
  function updateInputValueFromRaw() {
@@ -48,37 +83,120 @@
48
  }
49
 
50
  function handleInputChange(e: CustomEvent) {
51
- inputValue = e.detail;
52
  updateRawFromInputValue();
53
  }
54
 
55
  let generator: any = null;
56
  let loadingModel = false;
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  async function analyzeAndClean() {
 
 
59
  showSteps = false;
60
  cleaningSteps = [];
61
  cleanedValue = { data: [[]], headers: [] };
62
  cleanedData = '';
63
- loadingModel = true;
64
- try {
65
- if (!generator) {
66
- generator = await pipeline('text-generation', 'Xenova/gpt2');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
- loadingModel = false;
69
- // Prepare a prompt for the model
70
- const tableString = [inputValue.headers, ...inputValue.data].map(row => row.join(',')).join('\n');
71
- const prompt = `Clean this table and suggest steps. Table:\n${tableString}\nDescribe the cleaning steps and show a cleaned version.`;
72
- const output = await generator(prompt, { max_new_tokens: 128 });
73
- let content = output?.[0]?.generated_text || '';
74
- cleaningSteps = [{ step: content, accepted: true }];
75
- cleanedValue = { headers: inputValue.headers, data: inputValue.data };
76
- cleanedData = content;
77
- showSteps = true;
78
- } catch (err) {
79
- alert('Failed to load or run the model. Please check your internet connection and model support.');
80
- loadingModel = false;
81
  }
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
  function toggleStep(idx: number) {
@@ -101,6 +219,7 @@
101
  }
102
 
103
  $: updateInputValueFromRaw();
 
104
  </script>
105
 
106
  <div class="df-theme">
@@ -113,17 +232,39 @@
113
  <input type="file" accept=".csv,.tsv,.txt" bind:this={fileInput} on:change={handleFileUpload} />
114
  </div>
115
  <div style="margin: 1rem 0; width: 100%;">
116
- <Dataframe
117
- bind:value={inputValue}
118
- show_search="search"
119
- show_row_numbers={true}
120
- show_copy_button={true}
121
- show_fullscreen_button={true}
122
- editable={true}
123
- on:change={handleInputChange}
124
- />
 
 
 
125
  </div>
126
- <button on:click={analyzeAndClean}>Analyze &amp; Clean</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </section>
128
 
129
  {#if showSteps}
@@ -131,39 +272,29 @@
131
  <h2>AI-Suggested Cleaning Steps</h2>
132
  <ul>
133
  {#each cleaningSteps as step, idx}
134
- <li>
135
  <input type="checkbox" bind:checked={step.accepted} on:change={() => toggleStep(idx)} />
136
  {step.step}
137
- </li>
138
  {/each}
139
  </ul>
140
  </section>
141
  {/if}
142
 
143
  <section class="dataframes-section">
144
- <div class="dataframe original">
145
- <h3>Original Data</h3>
146
- <Dataframe
147
- bind:value={inputValue}
148
- show_search="search"
149
- show_row_numbers={true}
150
- show_copy_button={true}
151
- show_fullscreen_button={true}
152
- editable={true}
153
- on:change={handleInputChange}
154
- />
155
- </div>
156
- <div class="dataframe cleaned">
157
- <h3>Cleaned Data (Preview)</h3>
158
- <Dataframe
159
- bind:value={cleanedValue}
160
- show_search="search"
161
- show_row_numbers={true}
162
- show_copy_button={true}
163
- show_fullscreen_button={true}
164
- editable={false}
165
- />
166
- </div>
167
  </section>
168
 
169
  <button on:click={exportCleaned}>Export Cleaned Data</button>
@@ -171,14 +302,7 @@
171
  </div>
172
 
173
  <style>
174
- main {
175
- max-width: 900px;
176
- margin: 2rem auto;
177
- padding: 2rem;
178
- background: #fff;
179
- border-radius: 12px;
180
- box-shadow: 0 2px 8px rgba(0,0,0,0.07);
181
- }
182
  h1 {
183
  text-align: center;
184
  margin-bottom: 2rem;
@@ -198,15 +322,20 @@
198
  }
199
  .dataframes-section {
200
  display: flex;
 
201
  gap: 2rem;
202
- justify-content: center;
203
  margin-bottom: 2rem;
204
  }
 
 
 
 
 
205
  .dataframe {
206
- flex: 1;
207
  display: flex;
208
  flex-direction: column;
209
- align-items: center;
210
  }
211
  textarea {
212
  width: 100%;
@@ -236,4 +365,5 @@
236
  --gr-df-table-text: #222 !important;
237
  background: #fff;
238
  }
 
239
  </style>
 
12
  let inputValue: { data: string[][]; headers: string[] } = { data: [[]], headers: [] };
13
  let cleanedValue: { data: string[][]; headers: string[] } = { data: [[]], headers: [] };
14
 
15
+ // Cleaning UI state
16
+ let cleaningInProgress = false;
17
+ let cleaningError: string | null = null;
18
+ let modelWarning: string | null = null;
19
+ let progressCurrent = 0;
20
+ let progressTotal = 0;
21
+
22
 
23
  function parseCSVorTSV(text: string) {
24
  if (!text) return { data: [[]], headers: [] };
 
27
  const sep = lines[0].includes('\t') ? '\t' : ',';
28
  const headers = lines[0].split(sep).map(h => h.trim());
29
  const data = lines.slice(1).map(line => line.split(sep).map(cell => cell.trim()));
30
+ return normalizeTable({ data, headers });
31
+ }
32
+
33
+ function hasNonEmptyTable(value: { data: string[][]; headers: string[] } | null | undefined): boolean {
34
+ if (!value) return false;
35
+ const headersOk = Array.isArray(value.headers) && value.headers.length > 0;
36
+ const rowsOk = Array.isArray(value.data) && value.data.length > 0;
37
+ if (!headersOk || !rowsOk) return false;
38
+ // At least one non-empty cell
39
+ return value.data.some((row) => Array.isArray(row) && row.some((cell) => String(cell ?? '').trim() !== ''));
40
+ }
41
+
42
+ function normalizeTable(value: { data: unknown[][]; headers: unknown[] } | null | undefined): { data: string[][]; headers: string[] } {
43
+ if (!value || !Array.isArray(value.headers) || value.headers.length === 0) {
44
+ return { data: [], headers: [] };
45
+ }
46
+ const headers: string[] = value.headers.map((h) => String(h ?? ''));
47
+ const width = headers.length;
48
+ const data: string[][] = (Array.isArray(value.data) ? value.data : []).map((row) => {
49
+ const safeRow = Array.isArray(row) ? row : [];
50
+ const padded: string[] = Array.from({ length: width }, (_, i) => String(safeRow[i] ?? ''));
51
+ return padded;
52
+ });
53
+ return { headers, data };
54
+ }
55
+
56
+ function padRowToWidth(row: unknown[] | null | undefined, width: number): string[] {
57
+ const safeRow = Array.isArray(row) ? row : [];
58
+ return Array.from({ length: width }, (_, i) => String(safeRow[i] ?? ''));
59
  }
60
 
61
  function updateInputValueFromRaw() {
 
83
  }
84
 
85
  function handleInputChange(e: CustomEvent) {
86
+ inputValue = normalizeTable(e.detail);
87
  updateRawFromInputValue();
88
  }
89
 
90
  let generator: any = null;
91
  let loadingModel = false;
92
 
93
+ // Heuristic fallback cleaner if model output is unusable
94
+ function fallbackClean(text: string): string {
95
+ if (!text) return '';
96
+ // Remove URLs
97
+ let out = text.replace(/https?:\/\/\S+/g, '');
98
+ // Remove emojis and non-text pictographs
99
+ out = out.replace(/[\p{Extended_Pictographic}]/gu, '');
100
+ // Normalize quotes and whitespace
101
+ out = out
102
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
103
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
104
+ .normalize('NFC')
105
+ .replace(/\s+/g, ' ')
106
+ .trim();
107
+ return out;
108
+ }
109
+
110
+ async function cleanTextWithModel(text: string): Promise<string> {
111
+ if (!text || !text.trim()) return '';
112
+ if (!generator) {
113
+ loadingModel = true;
114
+ generator = await pipeline('text-generation', 'Xenova/gpt2');
115
+ loadingModel = false;
116
+ }
117
+
118
+ const prompt =
119
+ 'You are a text cleaning assistant. Clean the following text by fixing casing, spelling, removing URLs/emojis, and normalizing spaces. Return ONLY the cleaned text and nothing else.\nInput:\n"' +
120
+ text.replace(/\n/g, ' ') +
121
+ '"\nOutput:';
122
+
123
+ try {
124
+ const output = await generator(prompt, {
125
+ max_new_tokens: 80,
126
+ temperature: 0.2,
127
+ top_p: 0.9,
128
+ repetition_penalty: 1.1,
129
+ do_sample: false
130
+ });
131
+ const generated = output?.[0]?.generated_text ?? '';
132
+ const continuation = generated.slice(prompt.length).trim();
133
+ // If model echoes input or returns empty, use fallback
134
+ if (!continuation || continuation.length < 1) return fallbackClean(text);
135
+ // Trim to first line to avoid rambling
136
+ const firstLine = continuation.split('\n')[0].trim();
137
+ // Guard against the model including the word "Output:" again
138
+ return firstLine.replace(/^Output:\s*/i, '').trim();
139
+ } catch (e) {
140
+ return fallbackClean(text);
141
+ }
142
+ }
143
+
144
  async function analyzeAndClean() {
145
+ cleaningError = null;
146
+ modelWarning = null;
147
  showSteps = false;
148
  cleaningSteps = [];
149
  cleanedValue = { data: [[]], headers: [] };
150
  cleanedData = '';
151
+
152
+ // Ensure there is a table to clean
153
+ if (!inputValue.headers?.length) {
154
+ cleaningError = 'No headers detected. Please paste a table with headers in the first row.';
155
+ return;
156
+ }
157
+
158
+ // Prepare output schema: add a cleaned column for every original header
159
+ const cleanedHeaders = inputValue.headers.map((h) => `${h}_cleaned`);
160
+ const outHeaders = [...inputValue.headers, ...cleanedHeaders];
161
+
162
+ // Iterate rows
163
+ cleaningInProgress = true;
164
+ progressCurrent = 0;
165
+ const numRows = inputValue.data?.length ?? 0;
166
+ const numCols = inputValue.headers.length;
167
+ progressTotal = numRows * numCols;
168
+ const outRows: string[][] = [];
169
+
170
+ for (const row of inputValue.data) {
171
+ const baseRow = padRowToWidth(row, numCols);
172
+ const cleanedCells: string[] = [];
173
+ for (let c = 0; c < numCols; c += 1) {
174
+ const cell = (baseRow?.[c] ?? '').toString();
175
+ let cleaned = '';
176
+ try {
177
+ cleaned = await cleanTextWithModel(cell);
178
+ } catch (e) {
179
+ if (!modelWarning) modelWarning = 'Model unavailable. Used deterministic fallback cleaning.';
180
+ cleaned = fallbackClean(cell);
181
+ }
182
+ if (!cleaned) cleaned = fallbackClean(cell);
183
+ cleanedCells.push(cleaned);
184
+ progressCurrent += 1;
185
  }
186
+ const newRow = [...baseRow, ...cleanedCells];
187
+ outRows.push(newRow);
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
+
190
+ cleanedValue = normalizeTable({ headers: outHeaders, data: outRows });
191
+ // Keep a CSV copy in cleanedData for export convenience
192
+ const sep = ',';
193
+ const lines = [outHeaders.join(sep), ...outRows.map(r => r.join(sep))];
194
+ cleanedData = lines.join('\n');
195
+
196
+ // Note for transparency
197
+ cleaningSteps = [{ step: `Cleaned all ${numCols} columns using a text-generation model.`, accepted: true }];
198
+ showSteps = true;
199
+ cleaningInProgress = false;
200
  }
201
 
202
  function toggleStep(idx: number) {
 
219
  }
220
 
221
  $: updateInputValueFromRaw();
222
+ // No-op reactive blocks here
223
  </script>
224
 
225
  <div class="df-theme">
 
232
  <input type="file" accept=".csv,.tsv,.txt" bind:this={fileInput} on:change={handleFileUpload} />
233
  </div>
234
  <div style="margin: 1rem 0; width: 100%;">
235
+
236
+ {#if hasNonEmptyTable(inputValue)}
237
+ <Dataframe
238
+ bind:value={inputValue}
239
+ show_search="search"
240
+ show_row_numbers={true}
241
+ show_copy_button={true}
242
+ show_fullscreen_button={true}
243
+ editable={true}
244
+ on:change={handleInputChange}
245
+ />
246
+ {/if}
247
  </div>
248
+ <button on:click={analyzeAndClean} disabled={cleaningInProgress || loadingModel || !inputValue.headers?.length}>
249
+ {#if loadingModel}
250
+ Loading model...
251
+ {:else if cleaningInProgress}
252
+ Cleaning {progressCurrent}/{progressTotal}...
253
+ {:else}
254
+ Analyze &amp; Clean
255
+ {/if}
256
+ </button>
257
+
258
+ {#if cleaningError}
259
+ <div role="alert" aria-live="assertive" style="margin-top: 0.5rem; color: #b00020;">
260
+ {cleaningError}
261
+ </div>
262
+ {/if}
263
+ {#if modelWarning}
264
+ <div role="status" aria-live="polite" style="margin-top: 0.5rem; color: #5f5f00;">
265
+ {modelWarning}
266
+ </div>
267
+ {/if}
268
  </section>
269
 
270
  {#if showSteps}
 
272
  <h2>AI-Suggested Cleaning Steps</h2>
273
  <ul>
274
  {#each cleaningSteps as step, idx}
275
+ <div>
276
  <input type="checkbox" bind:checked={step.accepted} on:change={() => toggleStep(idx)} />
277
  {step.step}
278
+ </div>
279
  {/each}
280
  </ul>
281
  </section>
282
  {/if}
283
 
284
  <section class="dataframes-section">
285
+ {#if hasNonEmptyTable(cleanedValue)}
286
+ <div class="dataframe cleaned">
287
+ <h3>Cleaned Data (Preview)</h3>
288
+ <Dataframe
289
+ bind:value={cleanedValue}
290
+ show_search="search"
291
+ show_row_numbers={true}
292
+ show_copy_button={true}
293
+ show_fullscreen_button={true}
294
+ editable={false}
295
+ />
296
+ </div>
297
+ {/if}
 
 
 
 
 
 
 
 
 
 
298
  </section>
299
 
300
  <button on:click={exportCleaned}>Export Cleaned Data</button>
 
302
  </div>
303
 
304
  <style>
305
+
 
 
 
 
 
 
 
306
  h1 {
307
  text-align: center;
308
  margin-bottom: 2rem;
 
322
  }
323
  .dataframes-section {
324
  display: flex;
325
+ flex-direction: column;
326
  gap: 2rem;
 
327
  margin-bottom: 2rem;
328
  }
329
+ @media (max-width: 900px) {
330
+ .dataframes-section {
331
+ grid-template-columns: 1fr;
332
+ }
333
+ }
334
  .dataframe {
335
+ width: 100%;
336
  display: flex;
337
  flex-direction: column;
338
+ align-items: stretch;
339
  }
340
  textarea {
341
  width: 100%;
 
365
  --gr-df-table-text: #222 !important;
366
  background: #fff;
367
  }
368
+ /* select styles removed with column selector */
369
  </style>
src/app.css CHANGED
@@ -2,13 +2,10 @@
2
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
  line-height: 1.5;
4
  font-weight: 400;
5
- color-scheme: light dark;
6
- color: rgba(255, 255, 255, 0.87);
7
  font-synthesis: none;
8
  text-rendering: optimizeLegibility;
9
  -webkit-font-smoothing: antialiased;
10
  -moz-osx-font-smoothing: grayscale;
11
- color: black;
12
  }
13
 
14
  a {
 
2
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
  line-height: 1.5;
4
  font-weight: 400;
 
 
5
  font-synthesis: none;
6
  text-rendering: optimizeLegibility;
7
  -webkit-font-smoothing: antialiased;
8
  -moz-osx-font-smoothing: grayscale;
 
9
  }
10
 
11
  a {