-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
742 lines (667 loc) · 68.5 KB
/
Copy pathApp.tsx
File metadata and controls
742 lines (667 loc) · 68.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
// ... imports ...
// We need to import `executePipeline` which is now async.
// And we need to use `useEffect` carefully to handle async updates.
import React, { useState, useEffect, useMemo, useRef } from 'react';
import AdvancedAnalysis from './components/AdvancedAnalysis';
import DataTransformers from './components/DataTransformers';
import DataGenerators from './components/DataGenerators';
import AlgorithmButton from './components/AlgorithmButton';
import Modal from './components/Modal';
import RawImagePreview from './components/RawImagePreview';
import DecompressionStep from './components/DecompressionStep';
import ByteHeatmap from './components/ByteHeatmap';
import DataComposition from './components/DataComposition';
import DataInspector from './components/DataInspector';
import StepConfiguration from './components/StepConfiguration';
import TestBenchModal from './components/TestBenchModal';
import {
AlgorithmType,
Pipeline,
PipelineStep,
CompressionResult,
StepResult,
CustomPreset,
ProjectData,
BruteForceSettings,
BruteForceRunResult
} from './types';
import { executePipeline } from './services/compressionAlgorithms';
import { calculateEntropy, getByteFrequencyData } from './services/analysisUtils';
import { analyzeCompressionWithAI, suggestOptimization } from './services/geminiService';
import { isHexLike, textToHex, hexToText } from './services/dataUtils';
import { ICONS, ALGORITHM_DETAILS, ENTROPY_DETAILS, PRESETS } from './constants';
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
Cell
} from 'recharts';
const COLORS = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#06b6d4'];
const PIPELINE_COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ef4444'];
const ALGO_GROUPS = {
"Compression (Lossless)": [AlgorithmType.RLE, AlgorithmType.LZW, AlgorithmType.HUFFMAN, AlgorithmType.ARITHMETIC, AlgorithmType.LZ77, AlgorithmType.LZSS],
"Transformations": [AlgorithmType.BWT, AlgorithmType.MTF, AlgorithmType.DELTA, AlgorithmType.PNG_FILTER, AlgorithmType.XOR],
"Lossy": [AlgorithmType.DCT, AlgorithmType.QUANTIZATION, AlgorithmType.SUB_SAMPLING, AlgorithmType.HAAR, AlgorithmType.LPC],
"Misc": [AlgorithmType.BASE64]
};
const App = () => {
// --- State Management ---
const [activeTab, setActiveTab] = useState('workbench');
const [inputMode, setInputMode] = useState<'text' | 'hex' | 'image'>('text');
const [inputText, setInputText] = useState<string>(PRESETS.TEXT);
const [customPresets, setCustomPresets] = useState<CustomPreset[]>([]);
// Pipeline State
const [pipelines, setPipelines] = useState<Pipeline[]>([
{ id: 'p1', name: 'Pipeline 1', steps: [], color: PIPELINE_COLORS[0] }
]);
const [activePipelineId, setActivePipelineId] = useState<string>('p1');
const [showComparison, setShowComparison] = useState(false);
// UI State
const [sidebarView, setSidebarView] = useState<'original' | 'compressed'>('original');
const [originalImageMetadata, setOriginalImageMetadata] = useState<{ width: number, height: number } | null>(null);
const [selectedStepId, setSelectedStepId] = useState<string | 'input'>('input');
// Analysis State
const [results, setResults] = useState<Record<string, CompressionResult>>({});
const [isComputing, setIsComputing] = useState(false);
const [pipelineProgress, setPipelineProgress] = useState({ current: 0, total: 0 }); // NEW
const [aiAnalysis, setAiAnalysis] = useState<string>("");
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [isOptimizing, setIsOptimizing] = useState(false);
// Modal State
const [modalContent, setModalContent] = useState<{ title: string, content: React.ReactNode } | null>(null);
const [editingStepId, setEditingStepId] = useState<string | null>(null);
// Save Preset Modal State
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
const [newPresetName, setNewPresetName] = useState("");
// Inspect Step State
const [inspectStep, setInspectStep] = useState<StepResult | null>(null);
const [decompressedView, setDecompressedView] = useState<Uint8Array | null>(null);
// Test Bench State
const [isTestBenchOpen, setIsTestBenchOpen] = useState(false);
// Brute Force State
const [isBruteForceOpen, setIsBruteForceOpen] = useState(false);
const [bfSettings, setBfSettings] = useState<BruteForceSettings>({
minLength: 1,
maxLength: 3,
selectedAlgorithms: [AlgorithmType.RLE, AlgorithmType.BWT, AlgorithmType.MTF, AlgorithmType.HUFFMAN],
topN: 5,
target: 'size',
direction: 'min'
});
const [bfProgress, setBfProgress] = useState({ current: 0, total: 0 });
const [bfResults, setBfResults] = useState<BruteForceRunResult[]>([]);
const [bfIsRunning, setBfIsRunning] = useState(false);
const [bfEta, setBfEta] = useState<number | null>(null); // NEW: ETA in seconds
const stopBfRef = useRef(false);
// File Input Ref for loading
const fileInputRef = useRef<HTMLInputElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
// Derived Values
const activePipeline = pipelines.find(p => p.id === activePipelineId) || pipelines[0];
const activeResult = results[activePipelineId];
// Convert Input to Uint8Array
const inputBytes = useMemo(() => {
try {
if (inputMode === 'hex' || inputMode === 'image') {
const clean = inputText.replace(/\s+/g, '');
if (clean.length % 2 !== 0) return new Uint8Array(0);
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < clean.length; i += 2) {
bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
}
return bytes;
} else {
const arr = new Uint8Array(inputText.length);
for (let i = 0; i < inputText.length; i++) arr[i] = inputText.charCodeAt(i);
return arr;
}
} catch (e) {
return new Uint8Array(0);
}
}, [inputText, inputMode]);
const initialEntropy = useMemo(() => calculateEntropy(inputBytes), [inputBytes]);
// Determine which data to analyze based on selection
const analysisData = useMemo(() => {
if (selectedStepId === 'input' || !activeResult) return inputBytes;
const stepRes = activeResult.steps.find(s => s.stepId === selectedStepId);
return stepRes ? stepRes.fullOutput : inputBytes;
}, [selectedStepId, activeResult, inputBytes]);
const analysisEntropy = useMemo(() => calculateEntropy(analysisData), [analysisData]);
const freqData = useMemo(() => getByteFrequencyData(analysisData), [analysisData]);
const selectedStepName = useMemo(() => {
if (selectedStepId === 'input') return "Input Data";
const step = activePipeline.steps.find(s => s.id === selectedStepId);
return step ? `Step Output: ${step.type}` : "Data Analysis";
}, [selectedStepId, activePipeline]);
const [algoPreviews, setAlgoPreviews] = useState<Record<string, { size: number, entropy: number }>>({});
// Effect for Previews (Async now)
useEffect(() => {
if (!analysisData || analysisData.length === 0 || analysisData.length > 100000) {
setAlgoPreviews({});
return;
}
const runPreviews = async () => {
const currentSize = analysisData.length;
const currentEntropy = calculateEntropy(analysisData);
const previews: Record<string, { size: number, entropy: number }> = {};
const algos = Object.values(AlgorithmType);
for (const type of algos) {
try {
const res = await executePipeline(analysisData, [{ id: 'preview', type }]);
previews[type] = {
size: res.compressedSize - currentSize,
entropy: res.entropyAfter - currentEntropy
};
} catch (e) {
previews[type] = { size: 0, entropy: 0 };
}
}
setAlgoPreviews(previews);
};
runPreviews();
}, [analysisData]);
// Re-run compression when pipeline or data changes (Async)
useEffect(() => {
let isMounted = true;
const runPipelines = async () => {
setIsComputing(true);
setPipelineProgress({ current: 0, total: 0 }); // Reset progress
const newResults: Record<string, CompressionResult> = {};
for (const p of pipelines) {
if (inputBytes.length > 0) {
try {
newResults[p.id] = await executePipeline(inputBytes, p.steps, (curr, total) => {
if (isMounted) setPipelineProgress({ current: curr, total: total });
});
} catch (e) {
console.error(e);
newResults[p.id] = {
originalSize: inputBytes.length,
compressedSize: 0,
ratio: 1,
entropyBefore: 0,
entropyAfter: 0,
steps: [],
finalBytes: new Uint8Array(0),
integrity: { isLossless: true, matchesOriginal: false, lossySteps: [], decompressedData: null, decompressionSteps: [] }
};
}
} else {
newResults[p.id] = {
originalSize: 0,
compressedSize: 0,
ratio: 1,
entropyBefore: 0,
entropyAfter: 0,
steps: [],
finalBytes: new Uint8Array(0),
integrity: { isLossless: true, matchesOriginal: false, lossySteps: [], decompressedData: null, decompressionSteps: [] }
};
}
}
if (isMounted) {
setResults(newResults);
setIsComputing(false);
setAiAnalysis("");
}
};
runPipelines();
return () => { isMounted = false; };
}, [pipelines, inputBytes]);
// ... Actions ...
const handleModeChange = (newMode: 'text' | 'hex' | 'image') => {
if (newMode === inputMode) return;
let newText = inputText;
if (newMode === 'hex') {
if (!isHexLike(inputText)) newText = textToHex(inputText);
} else if (newMode === 'text') {
const converted = hexToText(inputText);
if (converted !== null) newText = converted;
}
setInputText(newText);
setInputMode(newMode);
};
const addPipeline = () => {
if (pipelines.length >= 4) return;
const newId = `p${pipelines.length + 1}`;
setPipelines([...pipelines, { id: newId, name: `Pipeline ${pipelines.length + 1}`, steps: [], color: PIPELINE_COLORS[pipelines.length % PIPELINE_COLORS.length] }]);
setActivePipelineId(newId);
};
const updatePipelineSteps = (pid: string, newSteps: PipelineStep[]) => {
setPipelines(pipelines.map(p => p.id === pid ? { ...p, steps: newSteps } : p));
};
const addToPipeline = (type: AlgorithmType) => {
const newStep: PipelineStep = { id: Math.random().toString(36).substr(2, 9), type };
updatePipelineSteps(activePipelineId, [...activePipeline.steps, newStep]);
setSelectedStepId(newStep.id);
};
const removeFromPipeline = (stepId: string) => {
updatePipelineSteps(activePipelineId, activePipeline.steps.filter(s => s.id !== stepId));
if (selectedStepId === stepId) setSelectedStepId('input');
};
const moveStep = (index: number, direction: 'up' | 'down') => {
const steps = [...activePipeline.steps];
if (direction === 'up' && index > 0) {
[steps[index], steps[index - 1]] = [steps[index - 1], steps[index]];
updatePipelineSteps(activePipelineId, steps);
} else if (direction === 'down' && index < steps.length - 1) {
[steps[index], steps[index + 1]] = [steps[index + 1], steps[index]];
updatePipelineSteps(activePipelineId, steps);
}
};
const updateStepParams = (stepId: string, params: Record<string, any>) => {
const newSteps = activePipeline.steps.map(s => s.id === stepId ? { ...s, params: { ...s.params, ...params } } : s);
updatePipelineSteps(activePipelineId, newSteps);
setEditingStepId(null);
};
const handleSaveCustomPreset = () => { if (inputText) { setNewPresetName(""); setIsSaveModalOpen(true); } };
const confirmSavePreset = () => { if (newPresetName) { setCustomPresets([...customPresets, { id: Math.random().toString(36).substr(2, 9), name: newPresetName, content: inputText, mode: inputMode }]); setIsSaveModalOpen(false); } };
const loadCustomPreset = (preset: CustomPreset) => { setInputMode(preset.mode); setInputText(preset.content); setSelectedStepId('input'); };
const handleDeletePreset = (e: React.MouseEvent, id: string) => { e.stopPropagation(); setCustomPresets(customPresets.filter(p => p.id !== id)); };
const handleSaveProject = () => {
const projectData: ProjectData = { version: 1, timestamp: Date.now(), pipelines, customPresets, activePipelineId, inputState: { content: inputText, mode: inputMode } };
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `compresso-project-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
};
const handleLoadProject = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target?.result as string) as ProjectData;
if (!data.pipelines || !data.version) { alert("Invalid project file"); return; }
setPipelines(data.pipelines); setCustomPresets(data.customPresets || []);
if (data.activePipelineId) setActivePipelineId(data.activePipelineId);
if (data.inputState) { setInputText(data.inputState.content); setInputMode(data.inputState.mode); }
if (fileInputRef.current) fileInputRef.current.value = '';
} catch (err) { console.error(err); alert("Failed to load project file"); }
};
reader.readAsText(file);
};
const triggerLoad = () => fileInputRef.current?.click();
const handleAiAnalysis = async () => { if (!activeResult) return; setIsAnalyzing(true); const analysis = await analyzeCompressionWithAI(inputText, activeResult); setAiAnalysis(analysis); setIsAnalyzing(false); };
const handleAiOptimize = async () => { if (inputBytes.length === 0) return; setIsOptimizing(true); const suggestions = await suggestOptimization("mixed", inputText); const newSteps = suggestions.map(type => ({ id: Math.random().toString(36).substr(2, 9), type })); updatePipelineSteps(activePipelineId, newSteps); setIsOptimizing(false); };
const showAlgorithmInfo = (type: AlgorithmType) => {
const info = ALGORITHM_DETAILS[type];
setModalContent({ title: info.title, content: <div className="space-y-4"><p className="text-slate-300 text-sm leading-relaxed">{info.desc}</p><div className="my-4"><h4 className="text-xs uppercase text-slate-500 font-bold mb-2">Visual Representation</h4>{info.visual}</div><div className="grid grid-cols-2 gap-4"><div className="bg-slate-950 p-3 rounded border border-slate-800"><h4 className="text-xs uppercase text-indigo-400 font-bold mb-1">Entropy Effect</h4><p className="text-xs text-slate-400">{info.entropyEffect}</p></div><div className="bg-slate-950 p-3 rounded border border-slate-800"><h4 className="text-xs uppercase text-emerald-400 font-bold mb-1">Best For</h4><p className="text-xs text-slate-400">{info.bestFor}</p></div></div></div> });
};
const showEntropyInfo = () => { setModalContent({ title: ENTROPY_DETAILS.title, content: <div className="space-y-4"><p className="text-slate-300 text-sm leading-relaxed">{ENTROPY_DETAILS.description}</p><div className="my-4">{ENTROPY_DETAILS.visual}</div><div className="bg-slate-950 p-3 rounded border border-slate-800"><h4 className="text-xs uppercase text-indigo-400 font-bold mb-1">Why it matters</h4><p className="text-xs text-slate-400">{ENTROPY_DETAILS.significance}</p></div></div> }) };
// Brute Force logic
const getBfSorter = () => (a: BruteForceRunResult, b: BruteForceRunResult) => {
let valA, valB;
if (bfSettings.target === 'size') { valA = a.result.compressedSize; valB = b.result.compressedSize; } else { valA = a.result.entropyAfter; valB = b.result.entropyAfter; }
return bfSettings.direction === 'min' ? valA - valB : valB - valA;
};
const runBruteForce = async () => {
setBfIsRunning(true); setBfResults([]); stopBfRef.current = false; setBfEta(null);
const numAlgos = bfSettings.selectedAlgorithms.length;
if (numAlgos === 0) { alert("Select at least one algorithm."); setBfIsRunning(false); return; }
let total = 0; for (let l = bfSettings.minLength; l <= bfSettings.maxLength; l++) total += Math.pow(numAlgos, l);
setBfProgress({ current: 0, total });
let count = 0; const allResults: BruteForceRunResult[] = []; const chunkSize = 10;
const startTime = Date.now();
function* permute(len: number) {
const indices = new Array(len).fill(0); const n = numAlgos; if (n === 0) return;
while (true) {
yield indices.map(i => bfSettings.selectedAlgorithms[i]);
let i = len - 1; while (i >= 0 && indices[i] === n - 1) { indices[i] = 0; i--; }
if (i < 0) break; indices[i]++;
}
}
for (let l = bfSettings.minLength; l <= bfSettings.maxLength; l++) {
const generator = permute(l);
let batchCount = 0;
for (const steps of generator) {
if (stopBfRef.current) break;
const pipelineSteps: PipelineStep[] = steps.map(type => ({ id: Math.random().toString(36).substr(2, 9), type }));
try {
const res = await executePipeline(inputBytes, pipelineSteps);
allResults.push({ steps, result: res });
} catch(e) {}
count++; batchCount++;
if (batchCount >= chunkSize) {
setBfProgress({ current: count, total });
// ETA Calculation
const elapsed = Date.now() - startTime;
const avgTime = elapsed / count;
const remaining = total - count;
setBfEta(Math.ceil((remaining * avgTime) / 1000)); // Seconds
if (allResults.length > 500) { allResults.sort(getBfSorter()); allResults.splice(bfSettings.topN * 2); }
await new Promise(r => setTimeout(r, 0)); batchCount = 0;
}
}
if (stopBfRef.current) break;
}
allResults.sort(getBfSorter()); setBfResults(allResults.slice(0, bfSettings.topN)); setBfIsRunning(false); setBfProgress({ current: total, total }); setBfEta(0);
};
const applyBfPipeline = (steps: AlgorithmType[]) => { updatePipelineSteps(activePipelineId, steps.map(type => ({ id: Math.random().toString(36).substr(2, 9), type }))); setIsBruteForceOpen(false); };
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height;
const ctx = canvas.getContext('2d'); if (!ctx) return;
ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height);
const rgbData = new Uint8Array(img.width * img.height * 3);
for (let i = 0; i < img.width * img.height; i++) { rgbData[i * 3] = imageData.data[i * 4]; rgbData[i * 3 + 1] = imageData.data[i * 4 + 1]; rgbData[i * 3 + 2] = imageData.data[i * 4 + 2]; }
const hexString = Array.from(rgbData).map(b => b.toString(16).padStart(2, '0')).join(' ');
setInputText(hexString); setInputMode('image'); setOriginalImageMetadata({ width: img.width, height: img.height }); setSelectedStepId('input');
if (imageInputRef.current) imageInputRef.current.value = '';
}; img.src = event.target?.result as string;
}; reader.readAsDataURL(file);
};
const handleTransform = (data: Uint8Array) => {
const hexString = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
setInputText(hexString);
setInputMode('hex');
};
const handleDataGenerated = (data: string, mode: 'text' | 'hex') => { setInputText(data); setInputMode(mode); setActiveTab('workbench'); };
// --- Render Helpers ---
const activeDisplayData = sidebarView === 'original' ? inputBytes : (activeResult?.finalBytes || new Uint8Array(0));
const activeDisplayEntropy = sidebarView === 'original' ? initialEntropy : (activeResult?.entropyAfter || 0);
return (
<div className="flex flex-col h-screen bg-slate-950 text-slate-200 font-sans selection:bg-indigo-500/30">
{/* Same Modals */}
<Modal isOpen={!!modalContent} onClose={() => setModalContent(null)} title={modalContent?.title || ''}>{modalContent?.content}</Modal>
<Modal isOpen={isSaveModalOpen} onClose={() => setIsSaveModalOpen(false)} title="Save Data Preset">
<div className="flex flex-col gap-4">
<p className="text-sm text-slate-400">Give your custom data preset a name to access it later easily.</p>
<input type="text" value={newPresetName} onChange={(e) => setNewPresetName(e.target.value)} placeholder="e.g. My Sensor Data" className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-sm text-white focus:border-indigo-500 outline-none placeholder:text-slate-600" autoFocus />
<div className="flex justify-end gap-2 mt-2">
<button onClick={() => setIsSaveModalOpen(false)} className="px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">Cancel</button>
<button onClick={confirmSavePreset} className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-500 text-white rounded font-semibold transition-colors shadow-lg shadow-indigo-500/20">Save Preset</button>
</div>
</div>
</Modal>
<Modal isOpen={!!editingStepId} onClose={() => setEditingStepId(null)} title="Configure Algorithm Step">
{editingStepId && activePipeline.steps.find(s => s.id === editingStepId) && (<StepConfiguration step={activePipeline.steps.find(s => s.id === editingStepId)!} onUpdate={(params) => updateStepParams(editingStepId, params)} />)}
</Modal>
<Modal isOpen={!!inspectStep} onClose={() => setInspectStep(null)} title={`Step Output: ${inspectStep?.type}`}>
<DataInspector data={inspectStep?.fullOutput} />
</Modal>
<Modal isOpen={!!decompressedView} onClose={() => setDecompressedView(null)} title="Verification: Decompressed Output">
<div className="flex flex-col gap-2">
<p className="text-sm text-slate-400">This is the result of reversing the pipeline steps.</p>
<DataInspector data={decompressedView || new Uint8Array(0)} />
</div>
</Modal>
<Modal isOpen={isTestBenchOpen} onClose={() => setIsTestBenchOpen(false)} title="Pipeline Test Bench" maxWidth="max-w-2xl">
<TestBenchModal pipeline={activePipeline} onClose={() => setIsTestBenchOpen(false)} customPresets={customPresets} />
</Modal>
{/* Brute Force Modal - Updated for ETA */}
<Modal isOpen={isBruteForceOpen} onClose={() => !bfIsRunning && setIsBruteForceOpen(false)} title="Brute Force Pipeline Search" maxWidth="max-w-4xl">
<div className="flex flex-col gap-6">
<div className="bg-slate-950 p-4 rounded-lg border border-slate-800 grid grid-cols-2 gap-6">
<div className="space-y-4">
<h4 className="text-xs uppercase font-bold text-slate-500 mb-2">Search Parameters</h4>
<div className="grid grid-cols-2 gap-4">
<div><label className="text-xs text-slate-400 block mb-1">Min Steps</label><input type="number" min="1" max="5" value={bfSettings.minLength} onChange={e => setBfSettings({ ...bfSettings, minLength: parseInt(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-white" /></div>
<div><label className="text-xs text-slate-400 block mb-1">Max Steps</label><input type="number" min="1" max="5" value={bfSettings.maxLength} onChange={e => setBfSettings({ ...bfSettings, maxLength: parseInt(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-white" /></div>
</div>
<div><label className="text-xs text-slate-400 block mb-1">Result Limit (Top N)</label><input type="number" min="1" max="20" value={bfSettings.topN} onChange={e => setBfSettings({ ...bfSettings, topN: parseInt(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-white" /></div>
<div>
<label className="text-xs text-slate-400 block mb-1">Goal</label>
<div className="flex gap-2">
<select value={bfSettings.direction} onChange={e => setBfSettings({ ...bfSettings, direction: e.target.value as 'min' | 'max' })} className="bg-slate-900 border border-slate-700 rounded p-2 text-sm text-white flex-1"><option value="min">Minimize</option><option value="max">Maximize</option></select>
<select value={bfSettings.target} onChange={e => setBfSettings({ ...bfSettings, target: e.target.value as 'size' | 'entropy' })} className="bg-slate-900 border border-slate-700 rounded p-2 text-sm text-white flex-1"><option value="size">Data Size</option><option value="entropy">Entropy</option></select>
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="text-xs uppercase font-bold text-slate-500 mb-2">Algorithms to Test</h4>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
{Object.values(AlgorithmType).map(type => (
<label key={type} className="flex items-center gap-2 p-2 rounded bg-slate-900 hover:bg-slate-800 cursor-pointer text-xs">
<input type="checkbox" checked={bfSettings.selectedAlgorithms.includes(type)} onChange={(e) => { if (e.target.checked) { setBfSettings({ ...bfSettings, selectedAlgorithms: [...bfSettings.selectedAlgorithms, type] }); } else { setBfSettings({ ...bfSettings, selectedAlgorithms: bfSettings.selectedAlgorithms.filter(t => t !== type) }); } }} className="rounded border-slate-600 bg-slate-800 text-indigo-500 focus:ring-indigo-500" />
<span className="text-slate-300">{type}</span>
</label>
))}
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs text-slate-400">
<span>Progress</span>
<div className="flex gap-4">
{bfEta !== null && <span>ETA: {bfEta}s</span>}
<span>{bfProgress.current} / {bfProgress.total}</span>
</div>
</div>
<div className="h-2 w-full bg-slate-800 rounded-full overflow-hidden"><div className="h-full bg-indigo-500 transition-all duration-300" style={{ width: bfProgress.total > 0 ? `${(bfProgress.current / bfProgress.total) * 100}%` : '0%' }}></div></div>
</div>
<div className="flex justify-between">
<div className="text-xs text-slate-500 self-center">{bfIsRunning ? "Running tests..." : bfResults.length > 0 ? "Search complete." : "Ready to search."}</div>
<div className="flex gap-2">{bfIsRunning ? (<button onClick={() => { stopBfRef.current = true; }} className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white text-sm rounded font-bold">Stop</button>) : (<button onClick={runBruteForce} className="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded font-bold shadow-lg shadow-indigo-500/20">Start Search</button>)}</div>
</div>
{bfResults.length > 0 && (
<div className="border border-slate-800 rounded-lg overflow-hidden">
<table className="w-full text-left text-xs">
<thead className="bg-slate-900 text-slate-400"><tr><th className="p-3">Rank</th><th className="p-3">Pipeline</th><th className="p-3 text-right">Size</th><th className="p-3 text-right">Entropy</th><th className="p-3 text-right">Action</th></tr></thead>
<tbody className="divide-y divide-slate-800 bg-slate-950/50">{bfResults.map((r, i) => (<tr key={i} className="hover:bg-slate-900 transition-colors"><td className="p-3 font-mono text-slate-500">#{i + 1}</td><td className="p-3 text-slate-300">{r.steps.join(' → ')}</td><td className="p-3 text-right font-mono text-emerald-400">{r.result.compressedSize} B</td><td className="p-3 text-right font-mono text-indigo-400">{r.result.entropyAfter.toFixed(2)}</td><td className="p-3 text-right"><button onClick={() => applyBfPipeline(r.steps)} className="text-indigo-400 hover:text-white underline">Apply</button></td></tr>))}</tbody>
</table>
</div>
)}
</div>
</Modal>
<input type="file" ref={fileInputRef} style={{ display: 'none' }} accept=".json" onChange={handleLoadProject} />
<input type="file" ref={imageInputRef} style={{ display: 'none' }} accept="image/*" onChange={handleImageUpload} />
{/* Header */}
<header className="h-16 border-b border-slate-800 flex items-center px-6 justify-between bg-slate-900/50 backdrop-blur-md z-10">
<div className="flex items-center gap-3"><div className="bg-indigo-500 p-2 rounded-lg text-white"><ICONS.Brain className="w-5 h-5" /></div><div><h1 className="text-lg font-bold tracking-tight text-white">Compresso Lab</h1><p className="text-xs text-slate-400">Data Compression Research & Design Workbench</p></div></div>
<div className="flex items-center gap-3">
<button onClick={() => setIsTestBenchOpen(true)} className="flex items-center gap-2 text-xs bg-indigo-900/30 hover:bg-indigo-900/50 text-indigo-300 border border-indigo-500/20 py-1.5 px-3 rounded transition-colors"><ICONS.Flask className="w-3.5 h-3.5" />Test Pipeline</button>
<button onClick={() => setIsBruteForceOpen(true)} className="flex items-center gap-2 text-xs bg-emerald-900/20 hover:bg-emerald-900/40 text-emerald-400 border border-emerald-900/50 py-1.5 px-3 rounded transition-colors mr-2"><ICONS.Cpu className="w-3.5 h-3.5" />Brute Force Search</button>
<button onClick={triggerLoad} className="flex items-center gap-2 text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 px-3 rounded border border-slate-700 transition-colors"><ICONS.Upload className="w-3.5 h-3.5" />Load Project</button>
<button onClick={handleSaveProject} className="flex items-center gap-2 text-xs bg-indigo-600 hover:bg-indigo-500 text-white py-1.5 px-3 rounded shadow-sm transition-colors"><ICONS.Download className="w-3.5 h-3.5" />Save Project</button>
</div>
</header>
{/* Tabs */}
<div className="flex items-center border-b border-slate-800 bg-slate-900/30 px-6 gap-4">
<button onClick={() => setActiveTab('workbench')} className={`py-3 text-sm font-medium border-b-2 ${activeTab === 'workbench' ? 'border-indigo-500 text-white' : 'border-transparent text-slate-400 hover:text-white'}`}><span className="flex items-center gap-2"><ICONS.Cpu className="w-4 h-4" /> Workbench</span></button>
<button onClick={() => setActiveTab('generators')} className={`py-3 text-sm font-medium border-b-2 ${activeTab === 'generators' ? 'border-indigo-500 text-white' : 'border-transparent text-slate-400 hover:text-white'}`}><span className="flex items-center gap-2"><ICONS.Dna className="w-4 h-4" /> Data Generators</span></button>
<button onClick={() => setActiveTab('analysis')} className={`py-3 text-sm font-medium border-b-2 ${activeTab === 'analysis' ? 'border-indigo-500 text-white' : 'border-transparent text-slate-400 hover:text-white'}`}><span className="flex items-center gap-2"><ICONS.Beaker className="w-4 h-4" /> Advanced Analysis</span></button>
</div>
<main className="flex-1 flex overflow-hidden">
{activeTab === 'workbench' && (
<>
<div className="w-80 flex flex-col border-r border-slate-800 bg-slate-900/30">
{/* Left Panel Content */}
{/* ... (Same as before) ... */}
<div className="p-2 border-b border-slate-800 bg-slate-950/50">
<div className="flex bg-slate-900 p-1 rounded-lg border border-slate-800 shadow-inner">
<button onClick={() => setSidebarView('original')} className={`flex-1 flex items-center justify-center gap-2 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded transition-all ${sidebarView === 'original' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}>Original</button>
<button onClick={() => setSidebarView('compressed')} className={`flex-1 flex items-center justify-center gap-2 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded transition-all ${sidebarView === 'compressed' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}>Compressed</button>
</div>
</div>
<div className="p-4 border-b border-slate-800">
{/* ... Input Controls ... */}
<h2 className="text-xs font-bold uppercase text-slate-500 mb-3 tracking-wider flex justify-between"><span>{sidebarView === 'original' ? 'Input Data' : 'Output Data'}</span>{sidebarView === 'compressed' && <span className="text-emerald-400 font-mono">{(activeResult?.ratio || 1).toFixed(2)}x</span>}</h2>
{sidebarView === 'original' ? (
<>
<div className="flex gap-1 mb-3 bg-slate-950 p-1 rounded-lg border border-slate-800">
<button onClick={() => handleModeChange('text')} className={`flex-1 text-xs py-1.5 rounded transition-all ${inputMode === 'text' ? 'bg-indigo-600 text-white shadow' : 'text-slate-500 hover:text-slate-300'}`}>Text</button>
<button onClick={() => handleModeChange('hex')} className={`flex-1 text-xs py-1.5 rounded transition-all ${inputMode === 'hex' ? 'bg-indigo-600 text-white shadow' : 'text-slate-500 hover:text-slate-300'}`}>Hex</button>
<button onClick={() => handleModeChange('image')} className={`flex-1 text-xs py-1.5 rounded transition-all ${inputMode === 'image' ? 'bg-indigo-600 text-white shadow' : 'text-slate-500 hover:text-slate-300'}`}>Image</button>
</div>
{inputMode === 'image' ? (
<div className="flex flex-col gap-3"><div className="w-full h-32 bg-slate-950 border border-slate-700 rounded p-4 flex flex-col items-center justify-center text-center gap-2"><ICONS.Image className="w-8 h-8 text-slate-600" /><p className="text-xs text-slate-400">Image preview moved to pipeline view</p></div><button onClick={() => imageInputRef.current?.click()} className="w-full flex items-center justify-center gap-2 text-xs bg-indigo-600 hover:bg-indigo-500 text-white py-2 rounded shadow-lg transition-all font-bold"><ICONS.Upload className="w-3.5 h-3.5" />Upload New Photo</button></div>
) : (<textarea value={inputText} onChange={(e) => setInputText(e.target.value)} className="w-full h-32 bg-slate-950 border border-slate-700 rounded p-2 text-xs font-mono text-slate-300 focus:outline-none focus:border-indigo-500 resize-none transition-colors" placeholder={inputMode === 'text' ? "Enter text here..." : "00 FF A1 ..."} />)}
</>
) : (
<div className="flex flex-col gap-3"><div className="bg-slate-950 border border-slate-800 rounded p-3 font-mono text-[10px] text-slate-400 h-32 overflow-y-auto break-all shadow-inner">{activeResult?.finalBytes && activeResult.finalBytes.length > 0 ? Array.from(activeResult.finalBytes.slice(0, 512)).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ') + '...' : 'No Output'}</div><div className="grid grid-cols-2 gap-2"><div className="bg-slate-900 p-2 rounded border border-slate-800"><div className="text-[9px] text-slate-500 uppercase font-bold">Compressed Size</div><div className="text-sm font-mono text-emerald-400">{activeResult?.compressedSize || 0} B</div></div><div className="bg-slate-900 p-2 rounded border border-slate-800"><div className="text-[9px] text-slate-500 uppercase font-bold">Space Saving</div><div className="text-sm font-mono text-indigo-400">{activeResult ? (100 - (100 / activeResult.ratio)).toFixed(1) : 0}%</div></div></div></div>
)}
<div className="flex justify-between mt-2 text-[10px] text-slate-500 items-center"><span>{activeDisplayData.length} bytes</span><button onClick={showEntropyInfo} className="flex items-center gap-1 hover:text-indigo-400 transition-colors">Entropy: {activeDisplayEntropy.toFixed(2)}<ICONS.Info className="w-3 h-3" /></button></div>
{sidebarView === 'original' && (
<>
<button onClick={handleSaveCustomPreset} className="w-full mt-3 flex items-center justify-center gap-2 text-xs bg-slate-800 hover:bg-emerald-900/30 hover:text-emerald-400 hover:border-emerald-900 border border-slate-700 text-slate-400 py-1.5 rounded transition-all"><ICONS.Save className="w-3 h-3" />Save Current as Preset</button>
<div className="grid grid-cols-2 gap-2 mt-4">
{/* Presets Logic */}
{inputMode === 'image' ? (
<><button onClick={() => { setInputText(PRESETS.GRADIENT_IMAGE); setOriginalImageMetadata({ width: 32, height: 32 }); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-2 px-2 rounded text-slate-300 transition-colors border border-slate-700 flex flex-col items-center gap-1"><div className="w-full h-4 bg-gradient-to-r from-black to-white rounded-sm"></div>Gradient</button><button onClick={() => { setInputText(PRESETS.STRIPES_IMAGE); setOriginalImageMetadata({ width: 32, height: 32 }); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-2 px-2 rounded text-slate-300 transition-colors border border-slate-700 flex flex-col items-center gap-1"><div className="w-full h-4 rounded-sm flex"><div className="w-1/2 bg-black"></div><div className="w-1/2 bg-white"></div></div>Stripes</button><button onClick={() => { setInputText(PRESETS.NOISY_IMAGE); setOriginalImageMetadata({ width: 32, height: 32 }); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-2 px-2 rounded text-slate-300 transition-colors border border-slate-700 flex flex-col items-center gap-1"><div className="w-full h-4 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] rounded-sm opacity-50 text-[8px] flex items-center justify-center">Noise</div>Noisy</button></>
) : (
<><button onClick={() => { setInputText(PRESETS.TEXT); setOriginalImageMetadata(null); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-1 px-2 rounded text-slate-300 transition-colors">Text Sample</button><button onClick={() => { setInputText(PRESETS.REPETITIVE); setOriginalImageMetadata(null); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-1 px-2 rounded text-slate-300 transition-colors">Repetitive</button><button onClick={() => { setInputText(PRESETS.BINARY_PATTERN); setOriginalImageMetadata(null); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-1 px-2 rounded text-slate-300 transition-colors">Binary Pattern</button><button onClick={() => { setInputText(PRESETS.NUMERICAL_RAMP); setOriginalImageMetadata(null); setSelectedStepId('input'); }} className="text-xs bg-slate-800 hover:bg-slate-700 py-1 px-2 rounded text-slate-300 transition-colors">Linear Ramp</button></>
)}
{customPresets.map(preset => (<div key={preset.id} className="relative group"><button onClick={() => loadCustomPreset(preset)} className="w-full text-xs bg-slate-800 hover:bg-indigo-900/30 hover:text-indigo-300 hover:border-indigo-900/50 border border-slate-700 py-1 px-2 rounded text-slate-300 truncate transition-all text-left">{preset.name}</button><button onClick={(e) => handleDeletePreset(e, preset.id)} className="absolute -top-1 -right-1 bg-red-900 text-white rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"><ICONS.X className="w-2 h-2" /></button></div>))}
</div>
</>
)}
</div>
<div className="p-4 flex-1 overflow-y-auto">
<h2 className="text-xs font-bold uppercase text-slate-500 mb-3 tracking-wider">Algorithms</h2>
<div className="flex flex-col gap-6">
{Object.entries(ALGO_GROUPS).map(([groupName, algos]) => (
<div key={groupName}>
<h3 className="text-[10px] font-bold uppercase text-slate-600 mb-2 pl-1">{groupName}</h3>
<div className="grid grid-cols-1 gap-2">
{algos.filter(type => { if (inputMode === 'text') return type !== AlgorithmType.DCT && type !== AlgorithmType.QUANTIZATION && type !== AlgorithmType.SUB_SAMPLING && type !== AlgorithmType.HAAR && type !== AlgorithmType.LPC; return true; }).map(type => (
<AlgorithmButton key={type} type={type} onAdd={addToPipeline} onInfo={showAlgorithmInfo} previewDiff={algoPreviews[type]} />
))}
</div>
</div>
))}
</div>
<div className="mt-8 p-4 bg-indigo-900/10 border border-indigo-500/20 rounded-lg"><div className="flex items-center gap-2 mb-2 text-indigo-400"><ICONS.Sparkles className="w-4 h-4" /><span className="text-xs font-bold">AI Assistant</span></div><p className="text-[10px] text-indigo-200/70 mb-3 leading-relaxed">Let Gemini analyze your data and suggest the optimal pipeline.</p><button onClick={handleAiOptimize} disabled={isOptimizing} className="w-full bg-indigo-600 hover:bg-indigo-500 text-white text-xs py-2 rounded shadow flex items-center justify-center gap-2 disabled:opacity-50 transition-colors">{isOptimizing ? 'Thinking...' : 'Auto-Optimize Pipeline'}</button></div>
<DataTransformers inputBytes={inputBytes} onTransform={handleTransform} />
</div>
</div>
{/* Center Canvas */}
<div className="flex-1 flex flex-col bg-slate-950 relative">
{/* Tabs */}
<div className="flex items-center border-b border-slate-800 bg-slate-900/30 px-4 pt-2 gap-2 overflow-x-auto">
{pipelines.map(p => (<button key={p.id} onClick={() => { setActivePipelineId(p.id); setShowComparison(false); }} className={`px-4 py-2 text-xs font-bold rounded-t-lg border-t border-x flex items-center gap-2 transition-all ${activePipelineId === p.id && !showComparison ? 'bg-slate-950 border-slate-700 text-white' : 'bg-slate-900/50 border-transparent text-slate-500 hover:text-slate-300'}`}><div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }}></div>{p.name}</button>))}
{pipelines.length < 4 && (<button onClick={addPipeline} className="p-2 text-slate-500 hover:text-indigo-400"><ICONS.Plus className="w-4 h-4" /></button>)}
<div className="flex-1"></div>
<button onClick={() => setShowComparison(true)} className={`px-4 py-2 text-xs font-bold rounded-t-lg border-t border-x flex items-center gap-2 transition-all ${showComparison ? 'bg-slate-950 border-slate-700 text-indigo-400' : 'bg-slate-900/50 border-transparent text-slate-500 hover:text-slate-300'}`}><ICONS.Compare className="w-3 h-3" />Compare All</button>
</div>
<div className="absolute inset-0 top-10 opacity-20 pointer-events-none" style={{ backgroundImage: 'radial-gradient(#334155 1px, transparent 1px)', backgroundSize: '20px 20px' }}></div>
<div className="flex-1 overflow-y-auto p-8 relative z-0">
{isComputing ? (
<div className="flex flex-col items-center justify-center h-full gap-4 text-slate-500">
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
<p className="text-xs animate-pulse">Running algorithms (WASM)...</p>
{/* Main Pipeline Progress Bar */}
{pipelineProgress.total > 0 && (
<div className="w-64 space-y-1">
<div className="flex justify-between text-[10px]">
<span>Step {pipelineProgress.current} of {pipelineProgress.total}</span>
<span>{Math.round((pipelineProgress.current / pipelineProgress.total) * 100)}%</span>
</div>
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
<div className="h-full bg-indigo-500 transition-all duration-300" style={{ width: `${(pipelineProgress.current / pipelineProgress.total) * 100}%` }}></div>
</div>
</div>
)}
</div>
) : showComparison ? (
<div className="max-w-4xl mx-auto w-full">
{/* Comparison View */}
<h2 className="text-lg font-bold text-slate-200 mb-6 flex items-center gap-2"><ICONS.Compare className="w-5 h-5 text-indigo-400" />Pipeline Comparison</h2>
<div className="grid gap-6">
<div className="h-64 bg-slate-900/50 p-4 rounded-xl border border-slate-800">
<ResponsiveContainer width="100%" height="100%"><BarChart data={pipelines.map(p => ({ name: p.name, ratio: results[p.id]?.ratio || 1, fill: p.color }))}><CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} /><XAxis dataKey="name" tick={{ fontSize: 10, fill: '#94a3b8' }} /><YAxis label={{ value: 'Ratio (x)', angle: -90, position: 'insideLeft', fill: '#64748b', fontSize: 10 }} tick={{ fontSize: 10, fill: '#64748b' }} /><Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155' }} itemStyle={{ color: '#e2e8f0' }} /><Bar dataKey="ratio" radius={[4, 4, 0, 0]} /></BarChart></ResponsiveContainer>
</div>
<div className="overflow-hidden rounded-xl border border-slate-800"><table className="w-full text-left text-sm"><thead className="bg-slate-900 text-slate-400"><tr><th className="p-3 font-semibold">Pipeline</th><th className="p-3 font-semibold">Algorithms</th><th className="p-3 font-semibold text-right">Size</th><th className="p-3 font-semibold text-right">Ratio</th><th className="p-3 font-semibold text-right">Status</th></tr></thead><tbody className="divide-y divide-slate-800 bg-slate-900/30">{pipelines.map(p => { const res = results[p.id]; if (!res) return null; return (<tr key={p.id} className="hover:bg-slate-800/50 transition-colors"><td className="p-3 flex items-center gap-2"><div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }}></div><span className="font-medium text-slate-200">{p.name}</span></td><td className="p-3 text-slate-400 text-xs">{p.steps.length > 0 ? p.steps.map(s => s.type).join(' → ') : 'None'}</td><td className="p-3 text-right font-mono text-slate-300">{res.compressedSize} B</td><td className="p-3 text-right"><span className={`px-2 py-1 rounded text-xs font-bold ${res.ratio > 1 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>{res.ratio.toFixed(2)}x</span></td><td className="p-3 text-right font-mono">{res.integrity.matchesOriginal ? <span className="text-emerald-400 flex justify-end items-center gap-1"><ICONS.ShieldCheck className="w-3 h-3" /> Verified</span> : res.integrity.isLossless ? <span className="text-red-400 flex justify-end items-center gap-1"><ICONS.ShieldAlert className="w-3 h-3" /> Failed</span> : <span className="text-amber-500 flex justify-end items-center gap-1"><ICONS.AlertTriangle className="w-3 h-3" /> Lossy</span>}</td></tr>) })}</tbody></table></div>
</div>
</div>
) : (
<div className="max-w-3xl mx-auto">
{/* Start Node */}
<div className="flex justify-center mb-8"><div onClick={() => setSelectedStepId('input')} className={`bg-slate-800 text-slate-400 border px-4 py-2 rounded-full text-xs font-mono shadow-lg flex items-center gap-2 cursor-pointer transition-all hover:bg-slate-700 ${selectedStepId === 'input' ? 'border-indigo-500 ring-2 ring-indigo-500/20' : 'border-slate-700'}`}><span>INPUT ({inputBytes.length} B)</span></div></div>
{/* Input Image Preview */}
{inputMode === 'image' && originalImageMetadata && inputBytes.length > 0 && (<div className="flex justify-center mb-6"><RawImagePreview data={inputBytes} width={originalImageMetadata.width} height={originalImageMetadata.height} label="Input Source" /></div>)}
{/* Steps */}
<div className="flex flex-col items-center gap-4">
{activePipeline.steps.map((step, index) => {
const stepResult = activeResult?.steps.find(s => s.stepId === step.id);
const prevStepResult = index > 0 ? activeResult?.steps[index - 1] : null;
const prevSize = prevStepResult ? prevStepResult.outputSize : inputBytes.length;
const prevEntropy = prevStepResult ? prevStepResult.outputEntropy : initialEntropy;
const stepRatio = stepResult && stepResult.outputSize > 0 ? prevSize / stepResult.outputSize : 0;
return (
<React.Fragment key={step.id}>
<div className="w-0.5 h-6 bg-slate-700"></div>
<div onClick={() => setSelectedStepId(step.id)} className={`w-full max-w-lg bg-slate-900 border rounded-lg p-4 shadow-xl relative group transition-all cursor-pointer ${selectedStepId === step.id ? 'border-indigo-500 ring-2 ring-indigo-500/20' : 'border-slate-800 hover:border-indigo-500/50'}`}>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-3"><div className="w-8 h-8 rounded bg-slate-800 flex items-center justify-center text-indigo-400 font-bold text-xs border border-slate-700">{index + 1}</div><div><div className="flex items-center gap-2"><h3 className="font-bold text-sm text-slate-200">{step.type}</h3>{stepResult?.isLossy && <span className="text-[10px] bg-amber-900/30 text-amber-500 px-1.5 py-0.5 rounded border border-amber-900/50 flex items-center gap-1"><ICONS.AlertTriangle className="w-3 h-3" /> Lossy</span>}<button onClick={(e) => { e.stopPropagation(); showAlgorithmInfo(step.type); }} className="text-slate-600 hover:text-indigo-400"><ICONS.Info className="w-3 h-3" /></button></div><p className="text-[10px] text-slate-500">{stepResult?.description || "Pending..."}</p></div></div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{step.type === AlgorithmType.XOR && (<button onClick={(e) => { e.stopPropagation(); setEditingStepId(step.id); }} className="p-1 hover:bg-slate-800 rounded text-slate-400 hover:text-indigo-400" title="Configure Step"><ICONS.Settings className="w-3 h-3" /></button>)}
<button onClick={(e) => { e.stopPropagation(); moveStep(index, 'up'); }} className="p-1 hover:bg-slate-800 rounded text-slate-400">↑</button>
<button onClick={(e) => { e.stopPropagation(); moveStep(index, 'down'); }} className="p-1 hover:bg-slate-800 rounded text-slate-400">↓</button>
<button onClick={(e) => { e.stopPropagation(); removeFromPipeline(step.id); }} className="p-1 hover:bg-red-900/30 text-red-400 rounded"><ICONS.Trash className="w-3 h-3" /></button>
</div>
</div>
{stepResult && (
<div className="mt-3 pt-3 border-t border-slate-800 grid grid-cols-3 gap-2">
<div><div className="text-[10px] text-slate-500 uppercase tracking-wider">Output Size</div><div className={`text-sm font-mono font-bold ${stepResult.outputSize < prevSize ? 'text-emerald-400' : 'text-red-400'}`}>{stepResult.outputSize} <span className="text-[10px] font-normal text-slate-500">bytes</span></div></div>
<div><div className="text-[10px] text-slate-500 uppercase tracking-wider">Ratio</div><div className="text-sm font-mono font-bold text-slate-200">{stepRatio.toFixed(2)}x</div></div>
<div><div className="text-[10px] text-slate-500 uppercase tracking-wider">Entropy</div><div className={`text-sm font-mono font-bold ${stepResult.outputEntropy < prevEntropy ? 'text-emerald-400' : 'text-red-400'}`}>{stepResult.outputEntropy.toFixed(3)}</div></div>
<div className="col-span-3"><div className="flex justify-between items-center mb-1"><div className="text-[10px] text-slate-500 uppercase tracking-wider">Preview (Hex)</div><button onClick={(e) => { e.stopPropagation(); setInspectStep(stepResult); }} className="text-[10px] text-indigo-400 hover:text-indigo-300 flex items-center gap-1"><ICONS.Eye className="w-3 h-3" /> Expand</button></div><div className="text-[10px] font-mono text-slate-400 bg-slate-950 p-1.5 rounded truncate cursor-pointer hover:bg-slate-900 transition-colors" onClick={(e) => { e.stopPropagation(); setInspectStep(stepResult); }}>{stepResult.previewHex} ...</div></div>
</div>
)}
</div>
</React.Fragment>
);
})}
{activePipeline.steps.length === 0 && (<div className="border-2 border-dashed border-slate-800 rounded-lg p-8 text-center text-slate-600 text-sm">Add algorithms from the left panel to build your pipeline.</div>)}
</div>
{/* End Node & Integrity */}
{activePipeline.steps.length > 0 && activeResult && (
<React.Fragment>
<div className="w-0.5 h-6 bg-slate-700 mx-auto my-2"></div>
<div className="flex flex-col gap-4 items-center">
<div className={`px-6 py-3 rounded-full text-xs font-bold shadow-lg flex items-center gap-3 border ${activeResult.ratio > 1 ? 'bg-emerald-900/20 text-emerald-400 border-emerald-500/30' : 'bg-red-900/20 text-red-400 border-red-500/30'}`}><span>OUTPUT ({activeResult.compressedSize} B)</span><span className="bg-black/20 px-2 py-0.5 rounded">{activeResult.ratio.toFixed(2)}x Ratio</span></div>
<div className={`w-full max-w-sm p-3 rounded border flex items-center justify-between ${activeResult.integrity.matchesOriginal ? 'bg-emerald-950/30 border-emerald-500/30' : !activeResult.integrity.isLossless ? 'bg-amber-950/30 border-amber-500/30' : 'bg-red-950/30 border-red-500/30'}`}>
<div className="flex items-center gap-3">{activeResult.integrity.matchesOriginal ? <div className="bg-emerald-500/20 p-2 rounded-full text-emerald-400"><ICONS.ShieldCheck className="w-5 h-5" /></div> : !activeResult.integrity.isLossless ? <div className="bg-amber-500/20 p-2 rounded-full text-amber-500"><ICONS.AlertTriangle className="w-5 h-5" /></div> : <div className="bg-red-500/20 p-2 rounded-full text-red-400"><ICONS.ShieldAlert className="w-5 h-5" /></div>}<div><div className={`text-xs font-bold ${activeResult.integrity.matchesOriginal ? 'text-emerald-400' : !activeResult.integrity.isLossless ? 'text-amber-500' : 'text-red-400'}`}>{activeResult.integrity.matchesOriginal ? 'Lossless Verified' : !activeResult.integrity.isLossless ? `Contains lossy steps: ${activeResult.integrity.lossySteps.join(', ')}` : 'Decompression failed to match original.'}</div></div></div>
{activeResult.integrity.decompressedData && (<button onClick={() => setDecompressedView(activeResult.integrity.decompressedData)} className="text-[10px] bg-slate-900 hover:bg-slate-800 text-slate-300 px-2 py-1 rounded border border-slate-700 transition-colors">Inspect</button>)}
</div>
</div>
</React.Fragment>
)}
{/* Decompression View */}
{activeResult && activeResult.integrity.decompressionSteps && activeResult.integrity.decompressionSteps.length > 0 && (
<div className="mt-12 w-full max-w-3xl mx-auto border-t border-slate-800 pt-8">
<h3 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-6 flex items-center gap-2"><ICONS.Refresh className="w-4 h-4" />Reverse Pipeline (Decompression)</h3>
<div className="space-y-4">{activeResult.integrity.decompressionSteps.map((step, index) => { const prevStep = index > 0 ? activeResult.integrity.decompressionSteps![index - 1] : null; const prevSize = prevStep ? prevStep.outputSize : activeResult.compressedSize; const prevEntropy = prevStep ? prevStep.outputEntropy : activeResult.entropyAfter; return (<div key={index} className="relative"><div className="absolute left-4 -top-4 w-0.5 h-4 bg-slate-800 border-l border-dashed border-slate-600"></div><DecompressionStep step={step} index={index} prevSize={prevSize} prevEntropy={prevEntropy} /></div>); })}</div>
<div className="flex justify-center mt-8 relative"><div className="absolute left-1/2 -top-8 w-0.5 h-8 bg-slate-800 border-l border-dashed border-slate-600"></div><div className={`px-6 py-3 rounded-full text-xs font-bold shadow-lg flex items-center gap-3 border bg-slate-900 border-slate-700 text-slate-300`}><span>RECONSTRUCTED DATA ({activeResult.integrity.decompressedData?.length || 0} B)</span></div></div>
{inputMode === 'image' && originalImageMetadata && activeResult.integrity.decompressedData && (<div className="flex justify-center mt-6"><RawImagePreview data={activeResult.integrity.decompressedData} width={originalImageMetadata.width} height={originalImageMetadata.height} label="Reconstructed Image" /></div>)}
</div>
)}
</div>
)}
</div>
</div>
{/* Right Panel */}
<div className="w-96 border-l border-slate-800 bg-slate-900/30 flex flex-col overflow-y-auto">
<div className="p-5 border-b border-slate-800">
<h2 className="text-xs font-bold uppercase text-slate-500 mb-4 tracking-wider flex items-center gap-2"><ICONS.BarChart className="w-4 h-4" />{selectedStepName}</h2>
<div className="mb-6"><div className="text-[10px] text-slate-400 mb-1">Data Composition</div><DataComposition data={analysisData} /></div>
<div className="mb-6"><div className="flex justify-between items-center mb-1"><div className="text-[10px] text-slate-400">Byte Map</div><div className="text-[10px] text-slate-600"><ICONS.Grid className="w-3 h-3" /></div></div><ByteHeatmap data={analysisData} /></div>
<div className="h-40 w-full mb-4"><ResponsiveContainer width="100%" height="100%"><BarChart data={freqData}><CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} /><XAxis dataKey="hex" tick={{ fontSize: 8, fill: '#64748b' }} interval={0} /><YAxis tick={{ fontSize: 8, fill: '#64748b' }} /><Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', fontSize: '12px' }} itemStyle={{ color: '#e2e8f0' }} /><Bar dataKey="count" fill="#6366f1">{freqData.map((entry, index) => (<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />))}</Bar></BarChart></ResponsiveContainer></div>
<div className="flex justify-between text-xs text-slate-400"><span>High Frequency Bytes</span><span>Count</span></div>
<div className="mt-2 space-y-1">{freqData.slice(0, 5).map((item, i) => (<div key={i} className="flex justify-between text-xs"><span className="font-mono text-slate-300">0x{item.hex} ({item.byte})</span><span className="text-slate-500">{item.count}</span></div>))}</div>
</div>
{activeResult && !showComparison && selectedStepId === 'input' && (
<div className="p-5 flex-1">
<h2 className="text-xs font-bold uppercase text-slate-500 mb-4 tracking-wider flex items-center gap-2"><ICONS.Sparkles className="w-4 h-4" />AI Insights</h2>
{!aiAnalysis ? (<button onClick={handleAiAnalysis} disabled={isAnalyzing} className="w-full py-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded text-xs text-slate-300 transition-colors">{isAnalyzing ? "Analyzing..." : "Generate Analysis Report"}</button>) : (<div className="bg-slate-950 border border-slate-800 rounded p-4 text-xs leading-relaxed text-slate-300 animate-in fade-in duration-500"><div className="whitespace-pre-line">{aiAnalysis}</div><button onClick={() => setAiAnalysis("")} className="mt-3 text-indigo-400 hover:text-indigo-300 text-[10px] underline">Refresh</button></div>)}
<div className="mt-8"><h3 className="text-xs font-bold text-slate-400 mb-3">Pipeline Performance</h3><div className="space-y-4"><div><div className="flex justify-between text-xs mb-1"><span className="text-slate-500">Space Saving</span><span className="text-emerald-400">{((1 - 1 / activeResult.ratio) * 100).toFixed(1)}%</span></div><div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden"><div className="h-full bg-emerald-500" style={{ width: `${Math.min(((1 - 1 / activeResult.ratio) * 100), 100)}%` }}></div></div></div><div><div className="flex justify-between text-xs mb-1"><span className="text-slate-500">Entropy Reduction</span><span className="text-indigo-400">{(activeResult.entropyBefore - activeResult.entropyAfter).toFixed(2)} bits</span></div><div className="flex gap-1 h-1.5 w-full"><div className="bg-indigo-900 h-full rounded-l" style={{ width: `${(activeResult.entropyAfter / 8) * 100}%` }}></div><div className="bg-indigo-500 h-full rounded-r" style={{ width: `${((activeResult.entropyBefore - activeResult.entropyAfter) / 8) * 100}%` }}></div></div></div></div></div>
</div>
)}
{showComparison && (<div className="p-5"><div className="text-xs text-slate-500 text-center italic">Select a specific pipeline tab to see detailed steps and run AI analysis.</div></div>)}
</div>
</>
)}
{activeTab === 'generators' && (<DataGenerators onDataGenerated={handleDataGenerated} />)}
{activeTab === 'analysis' && (<AdvancedAnalysis inputBytes={inputBytes} inputText={inputText} />)}
</main>
</div>
);
};
export default App;