abidlabs HF Staff Claude commited on
Commit
6bc35aa
·
1 Parent(s): ed2096e

Transform into immersive dataframe chat interface

Browse files

- Simplified to show only Gradio dataframe component
- Added fullscreen immersive layout with no padding/margins
- Created interactive chat interface at bottom with orange/black Gradio theme
- Added 26 rows of sample data for demonstration
- Implemented mock AI responses for data queries
- Beautiful glassmorphism design with smooth animations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. src/App.svelte +204 -809
src/App.svelte CHANGED
@@ -1,832 +1,263 @@
1
  <script lang="ts">
2
  import Dataframe from '@gradio/dataframe';
3
- import { pipeline } from '@xenova/transformers';
4
-
5
- let rawData = '';
6
- let cleanedData = '';
7
- let cleaningSteps: any[] = [];
8
- let showSteps = false;
9
- let fileInput: HTMLInputElement;
10
-
11
-
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
- // Cleaning toggles
23
- let optDedupeRows = true;
24
- let optDedupeColumns = false;
25
- let optRemoveSymbols = true;
26
- let optCollapseSpaces = true;
27
- let optHtmlToText = true;
28
-
29
- // Extra LLM instructions to augment standard cleaning
30
- let extraInstructions: string = '';
31
-
32
-
33
- function parseCSVorTSV(text: string) {
34
- if (!text) return { data: [[]], headers: [] };
35
- const lines = text.trim().split(/\r?\n/);
36
- if (lines.length === 0) return { data: [[]], headers: [] };
37
- const sep = lines[0].includes('\t') ? '\t' : ',';
38
- const headers = lines[0].split(sep).map(h => h.trim());
39
- const data = lines.slice(1).map(line => line.split(sep).map(cell => cell.trim()));
40
- return normalizeTable({ data, headers });
41
- }
42
-
43
- function hasNonEmptyTable(value: { data: string[][]; headers: string[] } | null | undefined): boolean {
44
- if (!value) return false;
45
- const headersOk = Array.isArray(value.headers) && value.headers.length > 0;
46
- const rowsOk = Array.isArray(value.data) && value.data.length > 0;
47
- if (!headersOk || !rowsOk) return false;
48
- // At least one non-empty cell
49
- return value.data.some((row) => Array.isArray(row) && row.some((cell) => String(cell ?? '').trim() !== ''));
50
- }
51
-
52
- function normalizeTable(value: { data: unknown[][]; headers: unknown[] } | null | undefined): { data: string[][]; headers: string[] } {
53
- if (!value || !Array.isArray(value.headers) || value.headers.length === 0) {
54
- return { data: [], headers: [] };
55
- }
56
- const headers: string[] = value.headers.map((h) => String(h ?? ''));
57
- const width = headers.length;
58
- const data: string[][] = (Array.isArray(value.data) ? value.data : []).map((row) => {
59
- const safeRow = Array.isArray(row) ? row : [];
60
- const padded: string[] = Array.from({ length: width }, (_, i) => String(safeRow[i] ?? ''));
61
- return padded;
62
- });
63
- return { headers, data };
64
- }
65
-
66
- function padRowToWidth(row: unknown[] | null | undefined, width: number): string[] {
67
- const safeRow = Array.isArray(row) ? row : [];
68
- return Array.from({ length: width }, (_, i) => String(safeRow[i] ?? ''));
69
- }
70
-
71
- function htmlToText(input: string): string {
72
- return input.replace(/<[^>]*>/g, '');
73
- }
74
-
75
- function removeSymbols(input: string): string {
76
- // Keep letters, numbers, common punctuation; remove emojis and other symbols
77
- return input
78
- .replace(/[\p{Extended_Pictographic}]/gu, '')
79
- .replace(/[\u200B-\u200D\uFEFF]/g, '')
80
- .replace(/[^\p{L}\p{N}\s\-_'",.;:!?()\[\]{}@#$%&*/+\\=<>^~`|]/gu, '');
81
- }
82
-
83
- function collapseSpaces(input: string): string {
84
- return input.replace(/\s+/g, ' ').trim();
85
- }
86
-
87
- function dedupeRows(headers: string[], rows: string[][]): string[][] {
88
- const seen = new Set<string>();
89
- const out: string[][] = [];
90
- for (const r of rows) {
91
- const key = r.join('\u0001');
92
- if (!seen.has(key)) {
93
- seen.add(key);
94
- out.push(padRowToWidth(r, headers.length));
95
- }
96
- }
97
- return out;
98
- }
99
-
100
- function dedupeColumns(headers: string[], rows: string[][]): { headers: string[]; rows: string[][] } {
101
- const seen = new Set<string>();
102
- const keepIdx: number[] = [];
103
- const outHeaders: string[] = [];
104
- headers.forEach((h, i) => {
105
- if (!seen.has(h)) {
106
- seen.add(h);
107
- keepIdx.push(i);
108
- outHeaders.push(h);
109
- }
110
- });
111
- const outRows = rows.map((r) => keepIdx.map((i) => r[i] ?? ''));
112
- return { headers: outHeaders, rows: outRows };
113
- }
114
-
115
- function updateInputValueFromRaw() {
116
- inputValue = parseCSVorTSV(rawData);
117
- }
118
-
119
- function updateRawFromInputValue() {
120
- // Convert inputValue back to CSV string
121
- if (!inputValue.headers.length) return;
122
- const sep = ',';
123
- const lines = [inputValue.headers.join(sep), ...inputValue.data.map(row => row.join(sep))];
124
- rawData = lines.join('\n');
125
- }
126
-
127
- function handleFileUpload(event: Event) {
128
- const files = (event.target as HTMLInputElement).files;
129
- if (files && files.length > 0) {
130
- const reader = new FileReader();
131
- reader.onload = (e) => {
132
- rawData = e.target?.result as string;
133
- updateInputValueFromRaw();
134
- };
135
- reader.readAsText(files[0]);
136
- }
137
- }
138
-
139
- function handleInputChange(e: CustomEvent) {
140
- inputValue = normalizeTable(e.detail);
141
- updateRawFromInputValue();
142
- }
143
-
144
- let textCleaner: any = null;
145
- let loadingModel = false;
146
-
147
- // Heuristic fallback cleaner if model output is unusable
148
- function fallbackClean(text: string): string {
149
- if (!text) return '';
150
- // Remove URLs
151
- let out = text.replace(/https?:\/\/\S+/g, '');
152
- // Remove emojis and non-text pictographs
153
- out = out.replace(/[\p{Extended_Pictographic}]/gu, '');
154
- // Remove HTML tags and zero-width characters
155
- out = out.replace(/<[^>]*>/g, '').replace(/[\u200B-\u200D\uFEFF]/g, '');
156
- // Normalize quotes and whitespace
157
- out = out
158
- .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
159
- .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
160
- .replace(/[\u2013\u2014]/g, '-')
161
- .normalize('NFC')
162
- .replace(/"{2,}/g, '"')
163
- .replace(/'{2,}/g, "'")
164
- .replace(/[!?.]{2,}/g, (m: string) => m[0])
165
- .replace(/\s+/g, ' ')
166
- .trim();
167
- // Remove stray leading 'clean:' tokens if any echoed
168
- out = out.replace(/^\s*clean:\s*/i, '');
169
- return out;
170
- }
171
-
172
- function isLikelyNumericOrDate(text: string): boolean {
173
- const t = (text || '').trim();
174
- if (!t) return false;
175
- // Currency/number like $12.00, 12, 12.0 USD
176
- if (/^[\p{Sc}]?\s?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)?(?:\s?(?:USD|EUR|GBP|JPY|AUD|CAD))?$/iu.test(t)) return true;
177
- // Date-like: YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, MM-DD-YYYY, with ., / or -
178
- if (/^(?:\d{4}[\-\/.]\d{2}[\-\/.]\d{2}|\d{2}[\-\/.]\d{2}[\-\/.]\d{4})$/.test(t)) return true;
179
- return false;
180
- }
181
-
182
- function jaccardTokenSimilarity(a: string, b: string): number {
183
- const toTokens = (s: string) => (s.match(/[\p{L}\p{N}\-']+/gu) ?? []).map((t) => t.toLowerCase());
184
- const A = new Set(toTokens(a));
185
- const B = new Set(toTokens(b));
186
- if (A.size === 0 && B.size === 0) return 1;
187
- let inter = 0;
188
- for (const t of A) if (B.has(t)) inter += 1;
189
- const union = new Set([...A, ...B]).size;
190
- return union === 0 ? 0 : inter / union;
191
- }
192
-
193
- function isPlausibleClean(source: string, candidate: string): boolean {
194
- if (!candidate) return false;
195
- const len = candidate.length;
196
- const srcLen = source.length;
197
- if (len > Math.max(8, Math.floor(srcLen * 1.4)) || len < Math.floor(srcLen * 0.4)) return false;
198
- const punctOnly = candidate.replace(/[\p{L}\p{N}]/gu, '').length / Math.max(1, len) > 0.35;
199
- if (punctOnly) return false;
200
- const sim = jaccardTokenSimilarity(source, candidate);
201
- if (sim < 0.5) return false;
202
- return true;
203
- }
204
-
205
- async function cleanTextWithModel(text: string): Promise<string> {
206
- if (!text || !text.trim()) return '';
207
- // Skip model for numeric/date-ish content
208
- if (isLikelyNumericOrDate(text)) return fallbackClean(text);
209
- if (!textCleaner) {
210
- loadingModel = true;
211
- // Use a small instruction-capable text2text model for deterministic edits
212
- // If unavailable, this will throw and we fallback deterministically.
213
- textCleaner = await pipeline('text2text-generation', 'Xenova/t5-small');
214
- loadingModel = false;
215
- }
216
-
217
- const prompt =
218
- 'clean: ' +
219
- text
220
- .replace(/https?:\/\/\S+/g, '')
221
- .replace(/[\p{Extended_Pictographic}]/gu, '')
222
- .replace(/\s+/g, ' ')
223
- .trim() +
224
- (extraInstructions && extraInstructions.trim().length > 0
225
- ? `\nrules: ${extraInstructions.trim()}`
226
- : '');
227
-
228
- try {
229
- const output = await textCleaner(prompt, {
230
- max_new_tokens: 64,
231
- temperature: 0,
232
- do_sample: false
233
- });
234
- const generated = output?.[0]?.generated_text ?? '';
235
- let candidate = generated.split('\n')[0].trim().replace(/^"|"$/g, '');
236
- // Normalize whitespace and quotes
237
- candidate = candidate
238
- .replace(/^\s*clean:\s*/i, '')
239
- .replace(/\bclean:\s*/gi, '')
240
- .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
241
- .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
242
- .replace(/[\u2013\u2014]/g, '-')
243
- .replace(/<[^>]*>/g, '')
244
- .replace(/[\u200B-\u200D\uFEFF]/g, '')
245
- .replace(/"{2,}/g, '"')
246
- .replace(/'{2,}/g, "'")
247
- .replace(/[!?.]{2,}/g, (m: string) => m[0])
248
- .replace(/\s+/g, ' ')
249
- .trim();
250
- if (!isPlausibleClean(text, candidate)) return fallbackClean(text);
251
- return candidate;
252
- } catch (e) {
253
- return fallbackClean(text);
254
- }
255
- }
256
-
257
- async function analyzeAndClean() {
258
- cleaningError = null;
259
- modelWarning = null;
260
- showSteps = false;
261
- cleaningSteps = [];
262
- cleanedValue = { data: [[]], headers: [] };
263
- cleanedData = '';
264
-
265
- // Ensure there is a table to clean
266
- if (!inputValue.headers?.length) {
267
- cleaningError = 'No headers detected. Please paste a table with headers in the first row.';
268
- return;
269
- }
270
-
271
- // Start with current headers/rows
272
- let outHeaders = [...inputValue.headers];
273
- let workingRows = inputValue.data.map((r) => padRowToWidth(r, outHeaders.length));
274
-
275
- // Deterministic transforms applied first
276
- if (optDedupeColumns) {
277
- const dc = dedupeColumns(outHeaders, workingRows);
278
- outHeaders = dc.headers;
279
- workingRows = dc.rows.map((r) => padRowToWidth(r, outHeaders.length));
280
- }
281
- if (optDedupeRows) {
282
- workingRows = dedupeRows(outHeaders, workingRows);
283
- }
284
-
285
- // Iterate rows
286
- cleaningInProgress = true;
287
- progressCurrent = 0;
288
- const numRows = workingRows.length;
289
- const numCols = outHeaders.length;
290
- progressTotal = numRows * numCols;
291
- const outRows: string[][] = [];
292
-
293
- for (const row of workingRows) {
294
- const baseRow = padRowToWidth(row, numCols);
295
- const cleanedCells: string[] = [];
296
- for (let c = 0; c < numCols; c += 1) {
297
- let cell = (baseRow?.[c] ?? '').toString();
298
- // Apply deterministic cell transforms
299
- if (optHtmlToText) cell = htmlToText(cell);
300
- if (optRemoveSymbols) cell = removeSymbols(cell);
301
- if (optCollapseSpaces) cell = collapseSpaces(cell);
302
- let cleaned = '';
303
- try {
304
- cleaned = await cleanTextWithModel(cell);
305
- } catch (e) {
306
- if (!modelWarning) modelWarning = 'Model unavailable. Used deterministic fallback cleaning.';
307
- cleaned = fallbackClean(cell);
308
- }
309
- if (!cleaned) cleaned = fallbackClean(cell);
310
- cleanedCells.push(cleaned);
311
- progressCurrent += 1;
312
- }
313
- const newRow = cleanedCells;
314
- outRows.push(newRow);
315
- }
316
-
317
- cleanedValue = normalizeTable({ headers: outHeaders, data: outRows });
318
- // Keep a CSV copy in cleanedData for export convenience
319
- const sep = ',';
320
- const lines = [outHeaders.join(sep), ...outRows.map(r => r.join(sep))];
321
- cleanedData = lines.join('\n');
322
-
323
- // Note for transparency
324
- cleaningSteps = [{ step: `Cleaned all ${numCols} columns and replaced values in the preview.`, accepted: true }];
325
- showSteps = true;
326
- cleaningInProgress = false;
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  setTimeout(() => {
329
- const resultsSection = document.querySelector('.results-card');
330
- if (resultsSection) {
331
- resultsSection.scrollIntoView({
332
- behavior: 'smooth',
333
- block: 'start',
334
- inline: 'nearest'
335
- });
336
- }
337
- }, 100);
338
- }
339
-
340
- function toggleStep(idx: number) {
341
- cleaningSteps[idx].accepted = !cleaningSteps[idx].accepted;
342
- }
343
-
344
- function exportCleaned() {
345
- // Export cleanedValue as CSV
346
- if (!cleanedValue.headers.length) return;
347
- const sep = ',';
348
- const lines = [cleanedValue.headers.join(sep), ...cleanedValue.data.map(row => row.join(sep))];
349
- const csv = lines.join('\n');
350
- const blob = new Blob([csv], { type: 'text/csv' });
351
- const url = URL.createObjectURL(blob);
352
- const a = document.createElement('a');
353
- a.href = url;
354
- a.download = 'cleaned_data.csv';
355
- a.click();
356
- URL.revokeObjectURL(url);
357
  }
358
-
359
- $: updateInputValueFromRaw();
360
- // No-op reactive blocks here
361
  </script>
362
 
363
- <div class="app-container df-theme">
364
- <header class="app-header">
365
- <div class="header-content">
366
- <h1>Tabular Data Cleaner</h1>
367
- <p class="subtitle">Clean and transform your spreadsheet data with AI assistance</p>
368
- </div>
369
- </header>
 
 
370
 
371
- <main class="app-main">
372
- <div class="content-grid">
373
- <!-- Input Section -->
374
- <section class="input-card">
375
- <div class="card-header">
376
- <h2>Import Data</h2>
377
- <p class="card-subtitle">Upload a file or paste your CSV/TSV data</p>
378
- </div>
379
- <div class="upload-area">
380
- <input type="file" accept=".csv,.tsv,.txt" bind:this={fileInput} on:change={handleFileUpload} id="file-input" class="file-input" />
381
- <label for="file-input" class="file-label">
382
- <div class="upload-icon">📁</div>
383
- <span>Choose file or drag & drop</span>
384
- </label>
385
- </div>
386
- <div class="divider">
387
- <span>or</span>
388
- </div>
389
- <textarea
390
- id="data-input"
391
- bind:value={rawData}
392
- rows="8"
393
- class="data-textarea"
394
- placeholder="Paste CSV or TSV data here..."
395
- on:input={updateInputValueFromRaw}
396
- ></textarea>
397
-
398
- {#if hasNonEmptyTable(inputValue)}
399
- <div class="preview-section">
400
- <h3>Data Preview</h3>
401
- <div class="dataframe-wrapper">
402
- <Dataframe
403
- bind:value={inputValue}
404
- show_search="search"
405
- show_row_numbers={true}
406
- show_copy_button={true}
407
- show_fullscreen_button={true}
408
- editable={true}
409
- on:change={handleInputChange}
410
- />
411
- </div>
412
- </div>
413
- {/if}
414
- </section>
415
-
416
- <!-- Controls Section -->
417
- <section class="controls-card">
418
- <div class="card-header">
419
- <h2>Cleaning Options</h2>
420
- </div>
421
-
422
- <div class="control-group">
423
- <h3>LLM Instructions</h3>
424
- <textarea
425
- rows="3"
426
- bind:value={extraInstructions}
427
- class="instructions-textarea"
428
- placeholder="e.g., expand abbreviations, fix capitalization..."
429
- ></textarea>
430
- </div>
431
-
432
- <div class="control-group">
433
- <h3>Transform Options</h3>
434
- <div class="checkbox-group">
435
- <label class="checkbox-label">
436
- <input type="checkbox" bind:checked={optDedupeRows} />
437
- <span class="checkmark"></span>
438
- Deduplicate rows
439
- </label>
440
- <label class="checkbox-label">
441
- <input type="checkbox" bind:checked={optDedupeColumns} />
442
- <span class="checkmark"></span>
443
- Deduplicate columns
444
- </label>
445
- <label class="checkbox-label">
446
- <input type="checkbox" bind:checked={optRemoveSymbols} />
447
- <span class="checkmark"></span>
448
- Remove symbols/emojis
449
- </label>
450
- <label class="checkbox-label">
451
- <input type="checkbox" bind:checked={optCollapseSpaces} />
452
- <span class="checkmark"></span>
453
- Remove extra spaces
454
- </label>
455
- <label class="checkbox-label">
456
- <input type="checkbox" bind:checked={optHtmlToText} />
457
- <span class="checkmark"></span>
458
- Convert HTML to text
459
- </label>
460
  </div>
461
  </div>
462
-
463
- <div class="action-buttons">
464
- <button
465
- class="btn-primary"
466
- on:click={analyzeAndClean}
467
- disabled={cleaningInProgress || loadingModel || !inputValue.headers?.length}
468
- >
469
- {#if loadingModel}
470
- <span class="spinner"></span>
471
- Loading model...
472
- {:else if cleaningInProgress}
473
- <span class="spinner"></span>
474
- Cleaning {progressCurrent}/{progressTotal}...
475
- {:else}
476
- ✨ Clean Data
477
- {/if}
478
- </button>
479
- <button
480
- class="btn-secondary"
481
- on:click={exportCleaned}
482
- disabled={!hasNonEmptyTable(cleanedValue)}
483
- >
484
- 💾 Export
485
- </button>
486
- </div>
487
-
488
- {#if cleaningError}
489
- <div class="alert alert-error">{cleaningError}</div>
490
- {/if}
491
- {#if modelWarning}
492
- <div class="alert alert-warning">{modelWarning}</div>
493
- {/if}
494
- </section>
495
  </div>
496
-
497
- <!-- Results Section -->
498
- {#if hasNonEmptyTable(cleanedValue)}
499
- <section class="results-card">
500
- <div class="card-header">
501
- <h2>Cleaned Results</h2>
502
- <p class="card-subtitle">Review and export your cleaned data</p>
503
- </div>
504
- <div class="dataframe-wrapper">
505
- <Dataframe
506
- bind:value={cleanedValue}
507
- show_search="search"
508
- show_row_numbers={true}
509
- show_copy_button={true}
510
- show_fullscreen_button={true}
511
- editable={false}
512
- />
513
- </div>
514
- </section>
515
- {/if}
516
- </main>
517
  </div>
518
 
519
  <style>
520
- /* Reset and base styles */
521
  :global(*) {
522
- box-sizing: border-box;
523
- }
524
-
525
- :global(body) {
526
  margin: 0;
527
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
528
- background: #f8fafc;
529
- color: #1e293b;
530
- line-height: 1.6;
531
- }
532
-
533
- /* App container */
534
- .app-container {
535
- min-height: 100vh;
536
- }
537
-
538
- /* Header */
539
- .app-header {
540
- margin: 0 auto;
541
- padding: 2rem;
542
- }
543
-
544
- .header-content {
545
- max-width: 1200px;
546
- margin: 0 auto;
547
- padding: 0 2rem;
548
- text-align: center;
549
- }
550
-
551
- .app-header h1 {
552
- font-size: 2.5rem;
553
- font-weight: 700;
554
- margin: 0 0 0.5rem 0;
555
- background: linear-gradient(135deg, #667eea, #764ba2);
556
- -webkit-background-clip: text;
557
- -webkit-text-fill-color: transparent;
558
- background-clip: text;
559
  }
560
 
561
- .subtitle {
562
- font-size: 1.1rem;
563
- color: #64748b;
564
  margin: 0;
565
- font-weight: 400;
566
- }
567
-
568
- /* Main content */
569
- .app-main {
570
- max-width: 1200px;
571
- margin: 0 auto;
572
- padding: 2rem;
573
- }
574
-
575
- .content-grid {
576
- display: grid;
577
- grid-template-columns: 650px 350px;
578
- gap: 2rem;
579
- margin-bottom: 2rem;
580
- justify-content: center;
581
- }
582
-
583
- @media (max-width: 768px) {
584
- .content-grid {
585
- grid-template-columns: 1fr;
586
- grid-template-rows: auto auto;
587
- max-width: 100%;
588
- }
589
-
590
- .input-card, .controls-card, .results-card {
591
- width: 100%;
592
- }
593
-
594
- .control-group {
595
- padding: 0;
596
- }
597
- }
598
-
599
-
600
- /* Card styles */
601
- .input-card, .controls-card, .results-card {
602
- background: white;
603
- border-radius: 16px;
604
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
605
- border: 1px solid rgba(226, 232, 240, 0.8);
606
  overflow: hidden;
 
607
  }
608
 
609
- .controls-card {
610
- width: 350px;
611
- height: min-content;
612
- }
613
-
614
- .card-header {
615
- padding: 1.5rem 1.5rem 1rem 1.5rem;
616
- border-bottom: 1px solid #f1f5f9;
617
- }
618
-
619
- .card-header h2 {
620
- font-size: 1.25rem;
621
- font-weight: 600;
622
- margin: 0 0 0.25rem 0;
623
- color: #1e293b;
624
- }
625
-
626
- .card-subtitle {
627
- font-size: 0.9rem;
628
- color: #64748b;
629
- margin: 0;
630
- }
631
-
632
- /* Upload area */
633
- .upload-area {
634
- padding: 1.5rem;
635
- }
636
-
637
- .file-input {
638
- display: none;
639
- }
640
-
641
- .file-label {
642
  display: flex;
643
  flex-direction: column;
644
- align-items: center;
645
- justify-content: center;
646
- padding: 2rem;
647
- border: 2px dashed #cbd5e1;
648
- border-radius: 12px;
649
- background: #f8fafc;
650
- cursor: pointer;
651
- transition: all 0.2s ease;
652
- }
653
-
654
- .file-label:hover {
655
- border-color: #667eea;
656
- background: #f1f5f9;
657
- }
658
-
659
- .upload-icon {
660
- font-size: 2rem;
661
- margin-bottom: 0.5rem;
662
- }
663
-
664
- .file-label span {
665
- color: #475569;
666
- font-weight: 500;
667
- }
668
-
669
- /* Divider */
670
- .divider {
671
- display: flex;
672
- align-items: center;
673
- margin: 0 1.5rem;
674
- text-align: center;
675
  }
676
 
677
- .divider::before,
678
- .divider::after {
679
- content: '';
680
  flex: 1;
681
- height: 1px;
682
- background: #e2e8f0;
683
- }
684
-
685
- .divider span {
686
- padding: 0 1rem;
687
- color: #64748b;
688
- font-size: 0.875rem;
689
- background: white;
690
- }
691
-
692
- /* Textareas */
693
-
694
- .data-textarea, .instructions-textarea {
695
- padding: 1rem;
696
- border: 1px solid #e2e8f0;
697
- border-radius: 8px;
698
- font-size: 0.875rem;
699
- resize: vertical;
700
- transition: border-color 0.2s ease;
701
- box-sizing: border-box;
702
- line-height: 1.5;
703
- min-height: 120px;
704
  }
705
 
706
- .data-textarea {
707
- margin: 1.5rem;
708
- width: calc(100% - 3rem);
709
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
710
  }
711
 
712
- .instructions-textarea {
713
- width: 100%;
714
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
715
  }
716
 
717
- .data-textarea:focus, .instructions-textarea:focus {
718
- outline: none;
719
- border-color: #667eea;
720
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
721
  }
722
 
723
- /* Control groups */
724
- .control-group {
725
- padding: 0 1.5rem 1.5rem 1.5rem;
 
 
 
726
  }
727
 
728
- .control-group h3 {
 
729
  font-size: 1rem;
730
- font-weight: 600;
731
- margin: 0 0 0.75rem 0;
732
- color: #374151;
733
- }
734
-
735
- /* Checkbox styles */
736
- .checkbox-group {
737
- display: flex;
738
- flex-direction: column;
739
- gap: 0.75rem;
740
  }
741
 
742
- .checkbox-label {
743
  display: flex;
744
- align-items: center;
745
- cursor: pointer;
746
- font-size: 0.9rem;
747
- color: #374151;
748
  }
749
 
750
- .checkbox-label input[type="checkbox"] {
751
- display: none;
 
 
 
 
 
 
 
 
752
  }
753
 
754
- .checkmark {
755
- width: 18px;
756
- height: 18px;
757
- border: 2px solid #d1d5db;
758
- border-radius: 4px;
759
- margin-right: 0.75rem;
760
- display: flex;
761
- align-items: center;
762
- justify-content: center;
763
- transition: all 0.2s ease;
764
  }
765
 
766
- .checkbox-label input[type="checkbox"]:checked + .checkmark {
767
- background: #667eea;
768
- border-color: #667eea;
769
  }
770
 
771
- .checkbox-label input[type="checkbox"]:checked + .checkmark::after {
772
- content: '✓';
 
 
 
773
  color: white;
774
- font-size: 12px;
775
- font-weight: bold;
776
- }
777
-
778
- /* Buttons */
779
- .action-buttons {
780
- padding: 0 1.5rem 1.5rem 1.5rem;
781
- display: flex;
782
- gap: 0.75rem;
783
- }
784
-
785
- .btn-primary, .btn-secondary {
786
- padding: 0.75rem 1.5rem;
787
- border-radius: 8px;
788
- border: none;
789
  font-weight: 600;
 
790
  cursor: pointer;
791
- transition: all 0.2s ease;
 
792
  display: flex;
793
  align-items: center;
794
  gap: 0.5rem;
795
  }
796
 
797
- .btn-primary {
798
- background: linear-gradient(135deg, #667eea, #764ba2);
799
- color: white;
800
- flex: 1;
801
  }
802
 
803
- .btn-primary:hover:not(:disabled) {
804
- transform: translateY(-1px);
805
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
806
- }
807
-
808
- .btn-secondary {
809
- background: #f8fafc;
810
- color: #475569;
811
- border: 1px solid #e2e8f0;
812
  }
813
 
814
- .btn-secondary:hover:not(:disabled) {
815
- background: #f1f5f9;
 
 
 
 
 
816
  }
817
 
818
- .btn-primary:disabled, .btn-secondary:disabled {
819
- opacity: 0.5;
820
- cursor: not-allowed;
821
- transform: none;
 
822
  }
823
 
824
- /* Spinner */
825
  .spinner {
826
- width: 16px;
827
- height: 16px;
828
- border: 2px solid transparent;
829
- border-top: 2px solid currentColor;
830
  border-radius: 50%;
831
  animation: spin 1s linear infinite;
832
  }
@@ -835,50 +266,14 @@
835
  to { transform: rotate(360deg); }
836
  }
837
 
838
- /* Alerts */
839
- .alert {
840
- margin: 0 1.5rem 1rem 1.5rem;
841
- padding: 0.75rem 1rem;
842
- border-radius: 8px;
843
- font-size: 0.875rem;
844
- }
845
-
846
- .alert-error {
847
- background: #fef2f2;
848
- color: #dc2626;
849
- border: 1px solid #fecaca;
850
- }
851
-
852
- .alert-warning {
853
- background: #fffbeb;
854
- color: #d97706;
855
- border: 1px solid #fed7aa;
856
- }
857
-
858
- /* Preview section */
859
- .preview-section {
860
- padding: 0 1.5rem 1.5rem 1.5rem;
861
- }
862
-
863
- .preview-section h3 {
864
- font-size: 1rem;
865
- font-weight: 600;
866
- margin: 0 0 1rem 0;
867
- color: #374151;
868
- }
869
-
870
-
871
- /* Results card */
872
- .results-card {
873
- grid-column: 1 / -1;
874
- }
875
-
876
- .results-card .dataframe-wrapper {
877
- margin: 1.5rem;
878
- }
879
-
880
- /* Theme overrides */
881
- .df-theme {
882
- --gr-df-table-text: #1e293b !important;
883
  }
884
- </style>
 
1
  <script lang="ts">
2
  import Dataframe from '@gradio/dataframe';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ // Sample data for the dataframe
5
+ let value: { data: string[][]; headers: string[] } = {
6
+ headers: ['Name', 'Age', 'City', 'Score', 'Status'],
7
+ data: [
8
+ ['Alice Johnson', '28', 'New York', '92.5', 'Active'],
9
+ ['Bob Smith', '34', 'Los Angeles', '87.3', 'Active'],
10
+ ['Carol Davis', '29', 'Chicago', '95.1', 'Inactive'],
11
+ ['David Wilson', '42', 'Houston', '78.9', 'Active'],
12
+ ['Eva Brown', '31', 'Phoenix', '88.7', 'Active'],
13
+ ['Frank Miller', '37', 'Philadelphia', '91.2', 'Inactive'],
14
+ ['Grace Lee', '26', 'San Antonio', '94.8', 'Active'],
15
+ ['Henry Taylor', '45', 'San Diego', '82.6', 'Active'],
16
+ ['Isabella Rodriguez', '33', 'Dallas', '89.4', 'Active'],
17
+ ['Jack Anderson', '29', 'San Jose', '93.7', 'Active'],
18
+ ['Kate Thompson', '36', 'Austin', '86.2', 'Inactive'],
19
+ ['Liam Martinez', '41', 'Jacksonville', '91.8', 'Active'],
20
+ ['Maya Patel', '27', 'Fort Worth', '94.3', 'Active'],
21
+ ['Noah Williams', '39', 'Columbus', '85.9', 'Active'],
22
+ ['Olivia Chen', '32', 'Charlotte', '90.1', 'Inactive'],
23
+ ['Peter Garcia', '44', 'San Francisco', '88.5', 'Active'],
24
+ ['Quinn Johnson', '30', 'Indianapolis', '92.7', 'Active'],
25
+ ['Rachel Kim', '35', 'Seattle', '87.8', 'Active'],
26
+ ['Samuel Brown', '38', 'Denver', '89.9', 'Inactive'],
27
+ ['Tara Wilson', '31', 'Washington', '93.2', 'Active'],
28
+ ['Ulysses Davis', '43', 'Boston', '86.4', 'Active'],
29
+ ['Victoria Lopez', '28', 'El Paso', '91.5', 'Active'],
30
+ ['William Moore', '40', 'Detroit', '84.7', 'Inactive'],
31
+ ['Ximena Taylor', '34', 'Nashville', '90.8', 'Active'],
32
+ ['Yuki Nakamura', '29', 'Portland', '92.1', 'Active'],
33
+ ['Zoe Jackson', '37', 'Oklahoma City', '88.3', 'Active']
34
+ ]
35
+ };
36
+
37
+ function handleChange(event: CustomEvent) {
38
+ value = event.detail;
39
+ }
40
+
41
+ let query = '';
42
+ let response = '';
43
+ let isSearching = false;
44
+
45
+ function searchData() {
46
+ if (!query.trim()) return;
47
+
48
+ isSearching = true;
49
+ response = '';
50
+
51
+ // Mock response for demo (will be replaced with LLM later)
52
  setTimeout(() => {
53
+ const mockResponses = [
54
+ "🔍 Based on the data, I found 21 active users and 5 inactive users. The most active city appears to be Los Angeles with high engagement scores.",
55
+ "📊 The average score across all users is 89.2, with Carol Davis having the highest score of 95.1 from Chicago.",
56
+ "👥 Looking at the age distribution, users range from 26 to 45 years old, with an average age of 34.3 years.",
57
+ "🏆 The top performers are primarily located in major metropolitan areas like New York, San Francisco, and Chicago.",
58
+ "📈 There's an interesting correlation between age and score - users in their late 20s tend to have higher scores on average."
59
+ ];
60
+
61
+ response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
62
+ isSearching = false;
63
+ }, 1000 + Math.random() * 1000);
64
+ }
65
+
66
+ function handleKeyPress(event: KeyboardEvent) {
67
+ if (event.key === 'Enter') {
68
+ searchData();
69
+ }
 
 
 
 
 
 
 
 
 
 
 
70
  }
 
 
 
71
  </script>
72
 
73
+ <div class="fullscreen-dataframe">
74
+ <div class="dataframe-container">
75
+ <Dataframe
76
+ bind:value={value}
77
+ show_row_numbers={true}
78
+ editable={true}
79
+ on:change={handleChange}
80
+ />
81
+ </div>
82
 
83
+ <div class="search-panel">
84
+ <div class="search-container">
85
+ <div class="search-header">
86
+ <h2>✨ Ask me anything about your data</h2>
87
+ <p>Try: "How many active users?", "Who has the highest score?", "What's the average age?"</p>
88
+ </div>
89
+
90
+ <div class="search-input-container">
91
+ <input
92
+ type="text"
93
+ bind:value={query}
94
+ on:keypress={handleKeyPress}
95
+ placeholder="Ask a question about the data..."
96
+ class="search-input"
97
+ disabled={isSearching}
98
+ />
99
+ <button
100
+ class="search-button"
101
+ on:click={searchData}
102
+ disabled={isSearching || !query.trim()}
103
+ >
104
+ {#if isSearching}
105
+ <div class="spinner"></div>
106
+ {:else}
107
+ 🔍 Ask
108
+ {/if}
109
+ </button>
110
+ </div>
111
+
112
+ {#if response}
113
+ <div class="response-container">
114
+ <div class="response-text">
115
+ {response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
  </div>
118
+ {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </div>
120
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
122
 
123
  <style>
 
124
  :global(*) {
 
 
 
 
125
  margin: 0;
126
+ padding: 0;
127
+ box-sizing: border-box;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  }
129
 
130
+ :global(body, html) {
 
 
131
  margin: 0;
132
+ padding: 0;
133
+ width: 100%;
134
+ height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  overflow: hidden;
136
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
137
  }
138
 
139
+ .fullscreen-dataframe {
140
+ width: 100vw;
141
+ height: 100vh;
142
+ position: fixed;
143
+ top: 0;
144
+ left: 0;
145
+ background: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  display: flex;
147
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
+ .dataframe-container {
 
 
151
  flex: 1;
152
+ overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
+ .search-panel {
156
+ background: linear-gradient(135deg, #ff6b35 0%, #1a1a1a 100%);
157
+ padding: 2rem;
158
+ box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.1);
159
  }
160
 
161
+ .search-container {
162
+ max-width: 800px;
163
+ margin: 0 auto;
164
  }
165
 
166
+ .search-header {
167
+ text-align: center;
168
+ margin-bottom: 1.5rem;
 
169
  }
170
 
171
+ .search-header h2 {
172
+ color: white;
173
+ font-size: 1.8rem;
174
+ font-weight: 700;
175
+ margin: 0 0 0.5rem 0;
176
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
177
  }
178
 
179
+ .search-header p {
180
+ color: rgba(255, 255, 255, 0.9);
181
  font-size: 1rem;
182
+ margin: 0;
183
+ opacity: 0.9;
 
 
 
 
 
 
 
 
184
  }
185
 
186
+ .search-input-container {
187
  display: flex;
188
+ gap: 1rem;
189
+ margin-bottom: 1.5rem;
 
 
190
  }
191
 
192
+ .search-input {
193
+ flex: 1;
194
+ padding: 1rem 1.5rem;
195
+ border: none;
196
+ border-radius: 50px;
197
+ font-size: 1.1rem;
198
+ background: white;
199
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
200
+ outline: none;
201
+ transition: all 0.3s ease;
202
  }
203
 
204
+ .search-input:focus {
205
+ box-shadow: 0 6px 25px rgba(0, 0, 0, 0.15);
206
+ transform: translateY(-2px);
 
 
 
 
 
 
 
207
  }
208
 
209
+ .search-input:disabled {
210
+ opacity: 0.7;
 
211
  }
212
 
213
+ .search-button {
214
+ padding: 1rem 2rem;
215
+ background: rgba(255, 255, 255, 0.2);
216
+ border: 2px solid rgba(255, 255, 255, 0.3);
217
+ border-radius: 50px;
218
  color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  font-weight: 600;
220
+ font-size: 1.1rem;
221
  cursor: pointer;
222
+ transition: all 0.3s ease;
223
+ backdrop-filter: blur(10px);
224
  display: flex;
225
  align-items: center;
226
  gap: 0.5rem;
227
  }
228
 
229
+ .search-button:hover:not(:disabled) {
230
+ background: rgba(255, 255, 255, 0.3);
231
+ border-color: rgba(255, 255, 255, 0.5);
232
+ transform: translateY(-2px);
233
  }
234
 
235
+ .search-button:disabled {
236
+ opacity: 0.5;
237
+ cursor: not-allowed;
 
 
 
 
 
 
238
  }
239
 
240
+ .response-container {
241
+ background: rgba(255, 255, 255, 0.95);
242
+ border-radius: 20px;
243
+ padding: 1.5rem 2rem;
244
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
245
+ backdrop-filter: blur(10px);
246
+ animation: slideUp 0.5s ease-out;
247
  }
248
 
249
+ .response-text {
250
+ font-size: 1.1rem;
251
+ line-height: 1.6;
252
+ color: #2d3748;
253
+ margin: 0;
254
  }
255
 
 
256
  .spinner {
257
+ width: 20px;
258
+ height: 20px;
259
+ border: 2px solid rgba(255, 255, 255, 0.3);
260
+ border-top: 2px solid white;
261
  border-radius: 50%;
262
  animation: spin 1s linear infinite;
263
  }
 
266
  to { transform: rotate(360deg); }
267
  }
268
 
269
+ @keyframes slideUp {
270
+ from {
271
+ opacity: 0;
272
+ transform: translateY(20px);
273
+ }
274
+ to {
275
+ opacity: 1;
276
+ transform: translateY(0);
277
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
+ </style>