mirror of
https://github.com/NotAShelf/nix-evaluator-stats.git
synced 2026-04-27 12:25:20 +00:00
jj commit -m "packages/web: add paste/upload snapshot flows; unify inputs"
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I7cb4e2ee6962e5d343cd52da6524b3596a6a6964
This commit is contained in:
parent
697f1f1c73
commit
1541046669
4 changed files with 288 additions and 156 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
import { Component, For, createSignal, createMemo, Show, onMount, onCleanup } from 'solid-js';
|
import { Component, For, createSignal, createMemo, Show, onMount, onCleanup } from 'solid-js';
|
||||||
import { ComparisonEntry, calculateChange } from '@ns/core';
|
import { ComparisonEntry, calculateChange, StatsData } from '@ns/core';
|
||||||
import { formatBytes, formatNumber, formatTime, formatPercent } from '@ns/ui-utils';
|
import { formatBytes, formatNumber, formatTime, formatPercent } from '@ns/ui-utils';
|
||||||
import ArrowRightIcon from 'lucide-solid/icons/arrow-right';
|
import ArrowRightIcon from 'lucide-solid/icons/arrow-right';
|
||||||
import ArrowDownIcon from 'lucide-solid/icons/arrow-down';
|
import ArrowDownIcon from 'lucide-solid/icons/arrow-down';
|
||||||
import ArrowUpIcon from 'lucide-solid/icons/arrow-up';
|
import ArrowUpIcon from 'lucide-solid/icons/arrow-up';
|
||||||
import XIcon from 'lucide-solid/icons/x';
|
import XIcon from 'lucide-solid/icons/x';
|
||||||
|
import FileUpload from './FileUpload';
|
||||||
|
|
||||||
interface ComparisonViewProps {
|
interface ComparisonViewProps {
|
||||||
entries: ComparisonEntry[];
|
entries: ComparisonEntry[];
|
||||||
|
|
@ -14,6 +15,8 @@ interface ComparisonViewProps {
|
||||||
pasteMode: 'advance' | 'replace';
|
pasteMode: 'advance' | 'replace';
|
||||||
onPasteModeChange: (mode: 'advance' | 'replace') => void;
|
onPasteModeChange: (mode: 'advance' | 'replace') => void;
|
||||||
onPasteStats: (text: string, name: string) => ComparisonEntry | null;
|
onPasteStats: (text: string, name: string) => ComparisonEntry | null;
|
||||||
|
onFileLoad: (data: StatsData, raw: Record<string, unknown>) => void;
|
||||||
|
onTextLoad: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComparisonView: Component<ComparisonViewProps> = props => {
|
const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||||
|
|
@ -150,159 +153,189 @@ const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="comparison-view">
|
<div class="comparison-view">
|
||||||
<div class="comparison-controls">
|
<Show when={props.entries.length >= 2}>
|
||||||
<div class="compare-selector">
|
<div class="comparison-controls">
|
||||||
<label>Baseline</label>
|
<div class="compare-selector">
|
||||||
<select onChange={selectLeft} value={leftEntry()?.id || ''}>
|
<label>Baseline</label>
|
||||||
<option value="">Select snapshot...</option>
|
<select onChange={selectLeft} value={leftEntry()?.id || ''}>
|
||||||
<For each={props.entries}>
|
<option value="">Select snapshot...</option>
|
||||||
{entry => <option value={entry.id}>{entry.name}</option>}
|
<For each={props.entries}>
|
||||||
</For>
|
{entry => <option value={entry.id}>{entry.name}</option>}
|
||||||
</select>
|
</For>
|
||||||
</div>
|
</select>
|
||||||
<div class="compare-arrow">
|
</div>
|
||||||
<ArrowRightIcon size={20} />
|
<div class="compare-arrow">
|
||||||
</div>
|
<ArrowRightIcon size={20} />
|
||||||
<div class="compare-selector">
|
</div>
|
||||||
<label>Current</label>
|
<div class="compare-selector">
|
||||||
<select onChange={selectRight} value={rightEntry()?.id || ''}>
|
<label>Current</label>
|
||||||
<option value="">Select snapshot...</option>
|
<select onChange={selectRight} value={rightEntry()?.id || ''}>
|
||||||
<For each={props.entries}>
|
<option value="">Select snapshot...</option>
|
||||||
{entry => <option value={entry.id}>{entry.name}</option>}
|
<For each={props.entries}>
|
||||||
</For>
|
{entry => <option value={entry.id}>{entry.name}</option>}
|
||||||
</select>
|
</For>
|
||||||
</div>
|
</select>
|
||||||
<div class="compare-paste-toggle">
|
</div>
|
||||||
<button
|
<div class="compare-paste-toggle">
|
||||||
class={props.pasteMode === 'advance' ? 'active' : ''}
|
<button
|
||||||
onClick={() => props.onPasteModeChange('advance')}
|
class={props.pasteMode === 'advance' ? 'active' : ''}
|
||||||
title="Paste shifts current to baseline"
|
onClick={() => props.onPasteModeChange('advance')}
|
||||||
>
|
title="Paste shifts current to baseline"
|
||||||
Auto
|
>
|
||||||
</button>
|
Auto
|
||||||
<button
|
</button>
|
||||||
class={props.pasteMode === 'replace' ? 'active' : ''}
|
<button
|
||||||
onClick={() => props.onPasteModeChange('replace')}
|
class={props.pasteMode === 'replace' ? 'active' : ''}
|
||||||
title="Paste replaces current only"
|
onClick={() => props.onPasteModeChange('replace')}
|
||||||
>
|
title="Paste replaces current only"
|
||||||
Replace
|
>
|
||||||
</button>
|
Replace
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.entries.length > 0}>
|
|
||||||
<div class="snapshots-list">
|
|
||||||
<h4>Saved Snapshots</h4>
|
|
||||||
<For each={props.entries}>
|
|
||||||
{entry => (
|
|
||||||
<div class="snapshot-item">
|
|
||||||
<span class="snapshot-name">{entry.name}</span>
|
|
||||||
<button class="delete-btn" onClick={() => props.onDelete(entry.id)}>
|
|
||||||
<XIcon size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={leftEntry() && rightEntry()}
|
when={props.entries.length >= 2}
|
||||||
fallback={<div class="compare-placeholder">Select two snapshots to compare metrics</div>}
|
fallback={
|
||||||
>
|
<div class="compare-upload-section">
|
||||||
<div class="comparison-table">
|
<FileUpload
|
||||||
<div class="compare-header">
|
onFileLoad={props.onFileLoad}
|
||||||
<div class="col-label">Metric</div>
|
onTextLoad={props.onTextLoad}
|
||||||
<div class="col-value">{leftEntry()?.name}</div>
|
snapshots={props.entries}
|
||||||
<div class="col-value">{rightEntry()?.name}</div>
|
onLoadSnapshot={props.onSelect}
|
||||||
<div class="col-change">Change</div>
|
/>
|
||||||
</div>
|
<Show when={props.entries.length === 1}>
|
||||||
<For each={comparison()}>
|
<div class="compare-more-needed">
|
||||||
{row => (
|
<div class="compare-placeholder-hint">
|
||||||
<div
|
Upload or paste one more snapshot to start comparing
|
||||||
class={`compare-row ${row.isDifferent ? 'diff' : ''} ${row.isMissing ? 'missing' : ''}`}
|
|
||||||
>
|
|
||||||
<div class="col-label" title={row.label}>
|
|
||||||
{row.label}
|
|
||||||
</div>
|
|
||||||
<div class="col-value">
|
|
||||||
<Show
|
|
||||||
when={row.isMissing}
|
|
||||||
fallback={
|
|
||||||
row.format === 'bytes'
|
|
||||||
? formatBytes(row.leftValue, prec())
|
|
||||||
: row.format === 'time'
|
|
||||||
? formatTime(row.leftValue, prec())
|
|
||||||
: row.format === 'percent'
|
|
||||||
? formatPercent(row.leftValue, prec())
|
|
||||||
: formatNumber(row.leftValue, prec())
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="missing-value">N/A</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="col-value">
|
|
||||||
<Show
|
|
||||||
when={row.isMissing}
|
|
||||||
fallback={
|
|
||||||
row.format === 'bytes'
|
|
||||||
? formatBytes(row.rightValue, prec())
|
|
||||||
: row.format === 'time'
|
|
||||||
? formatTime(row.rightValue, prec())
|
|
||||||
: row.format === 'percent'
|
|
||||||
? formatPercent(row.rightValue, prec())
|
|
||||||
: formatNumber(row.rightValue, prec())
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="missing-value">N/A</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class={`col-change ${row.isReduction ? 'good' : row.isDifferent && !row.isMissing ? 'bad' : ''}`}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={row.isMissing}
|
|
||||||
fallback={
|
|
||||||
<Show when={row.isDifferent} fallback={<span class="neutral">—</span>}>
|
|
||||||
<span class="change-value">
|
|
||||||
<Show when={row.isReduction}>
|
|
||||||
<ArrowDownIcon size={14} />
|
|
||||||
</Show>
|
|
||||||
<Show when={!row.isReduction}>
|
|
||||||
<ArrowUpIcon size={14} />
|
|
||||||
</Show>
|
|
||||||
{Math.abs(row.change).toFixed(prec())}%
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="missing-indicator"
|
|
||||||
title="Field not available in one or both implementations"
|
|
||||||
>
|
|
||||||
N/A
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Show>
|
||||||
</For>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
<Show when={props.entries.length > 0}>
|
||||||
|
<div class="snapshots-list">
|
||||||
|
<h4>Saved Snapshots</h4>
|
||||||
|
<For each={props.entries}>
|
||||||
|
{entry => (
|
||||||
|
<div class="snapshot-item">
|
||||||
|
<span class="snapshot-name">{entry.name}</span>
|
||||||
|
<button class="delete-btn" onClick={() => props.onDelete(entry.id)}>
|
||||||
|
<XIcon size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="comparison-summary">
|
<Show
|
||||||
<Show when={comparison()?.some(r => r.isReduction)}>
|
when={leftEntry() && rightEntry()}
|
||||||
<div class="summary-good">
|
fallback={
|
||||||
<ArrowDownIcon size={16} />{' '}
|
<div class="compare-placeholder">
|
||||||
{comparison()?.filter(r => r.isReduction && r.isDifferent).length} improved
|
<div>Select two snapshots to compare metrics</div>
|
||||||
|
<div class="compare-placeholder-hint">
|
||||||
|
Paste JSON stats here (Ctrl+V) while in compare mode
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
}
|
||||||
<Show when={comparison()?.some(r => !r.isReduction && r.isDifferent)}>
|
>
|
||||||
<div class="summary-bad">
|
<div class="comparison-table">
|
||||||
<ArrowUpIcon size={16} />{' '}
|
<div class="compare-header">
|
||||||
{comparison()?.filter(r => !r.isReduction && r.isDifferent).length} regressed
|
<div class="col-label">Metric</div>
|
||||||
|
<div class="col-value">{leftEntry()?.name}</div>
|
||||||
|
<div class="col-value">{rightEntry()?.name}</div>
|
||||||
|
<div class="col-change">Change</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<For each={comparison()}>
|
||||||
</div>
|
{row => (
|
||||||
|
<div
|
||||||
|
class={`compare-row ${row.isDifferent ? 'diff' : ''} ${row.isMissing ? 'missing' : ''}`}
|
||||||
|
>
|
||||||
|
<div class="col-label" title={row.label}>
|
||||||
|
{row.label}
|
||||||
|
</div>
|
||||||
|
<div class="col-value">
|
||||||
|
<Show
|
||||||
|
when={row.isMissing}
|
||||||
|
fallback={
|
||||||
|
row.format === 'bytes'
|
||||||
|
? formatBytes(row.leftValue, prec())
|
||||||
|
: row.format === 'time'
|
||||||
|
? formatTime(row.leftValue, prec())
|
||||||
|
: row.format === 'percent'
|
||||||
|
? formatPercent(row.leftValue, prec())
|
||||||
|
: formatNumber(row.leftValue, prec())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="missing-value">N/A</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="col-value">
|
||||||
|
<Show
|
||||||
|
when={row.isMissing}
|
||||||
|
fallback={
|
||||||
|
row.format === 'bytes'
|
||||||
|
? formatBytes(row.rightValue, prec())
|
||||||
|
: row.format === 'time'
|
||||||
|
? formatTime(row.rightValue, prec())
|
||||||
|
: row.format === 'percent'
|
||||||
|
? formatPercent(row.rightValue, prec())
|
||||||
|
: formatNumber(row.rightValue, prec())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="missing-value">N/A</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`col-change ${row.isReduction ? 'good' : row.isDifferent && !row.isMissing ? 'bad' : ''}`}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={row.isMissing}
|
||||||
|
fallback={
|
||||||
|
<Show when={row.isDifferent} fallback={<span class="neutral">—</span>}>
|
||||||
|
<span class="change-value">
|
||||||
|
<Show when={row.isReduction}>
|
||||||
|
<ArrowDownIcon size={14} />
|
||||||
|
</Show>
|
||||||
|
<Show when={!row.isReduction}>
|
||||||
|
<ArrowUpIcon size={14} />
|
||||||
|
</Show>
|
||||||
|
{Math.abs(row.change).toFixed(prec())}%
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="missing-indicator"
|
||||||
|
title="Field not available in one or both implementations"
|
||||||
|
>
|
||||||
|
N/A
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comparison-summary">
|
||||||
|
<Show when={comparison()?.some(r => r.isReduction)}>
|
||||||
|
<div class="summary-good">
|
||||||
|
<ArrowDownIcon size={16} />{' '}
|
||||||
|
{comparison()?.filter(r => r.isReduction && r.isDifferent).length} improved
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={comparison()?.some(r => !r.isReduction && r.isDifferent)}>
|
||||||
|
<div class="summary-bad">
|
||||||
|
<ArrowUpIcon size={16} />{' '}
|
||||||
|
{comparison()?.filter(r => !r.isReduction && r.isDifferent).length} regressed
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={showPasteModal()}>
|
<Show when={showPasteModal()}>
|
||||||
|
|
@ -349,7 +382,7 @@ const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button class="confirm-btn" onClick={confirmPaste}>
|
<button class="confirm-btn" onClick={confirmPaste}>
|
||||||
Save & Compare
|
{props.entries.length >= 2 ? 'Save & Compare' : 'Save Snapshot'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,15 @@ import ClockIcon from 'lucide-solid/icons/clock';
|
||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
onFileLoad: (data: StatsData, raw: Record<string, unknown>) => void;
|
onFileLoad: (data: StatsData, raw: Record<string, unknown>) => void;
|
||||||
onTextLoad: (text: string) => void;
|
onTextLoad: (text: string) => void;
|
||||||
showHelp: boolean;
|
|
||||||
onToggleHelp: () => void;
|
|
||||||
snapshots?: ComparisonEntry[];
|
snapshots?: ComparisonEntry[];
|
||||||
onLoadSnapshot?: (entry: ComparisonEntry) => void;
|
onLoadSnapshot?: (entry: ComparisonEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FileUpload(props: FileUploadProps) {
|
export default function FileUpload(props: FileUploadProps) {
|
||||||
const [textInput, setTextInput] = createSignal('');
|
const [textInput, setTextInput] = createSignal('');
|
||||||
const [isTextMode, setIsTextMode] = createSignal(false);
|
const [isTextMode, setIsTextMode] = createSignal(true);
|
||||||
const [error, setError] = createSignal('');
|
const [error, setError] = createSignal('');
|
||||||
|
const [showHelp, setShowHelp] = createSignal(false);
|
||||||
|
|
||||||
const handleFile = async (e: Event) => {
|
const handleFile = async (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
|
|
@ -49,11 +48,11 @@ export default function FileUpload(props: FileUploadProps) {
|
||||||
</div>
|
</div>
|
||||||
<h2>Load Statistics</h2>
|
<h2>Load Statistics</h2>
|
||||||
|
|
||||||
<div class="help-link" onClick={props.onToggleHelp}>
|
<div class="help-link" onClick={() => setShowHelp(!showHelp())}>
|
||||||
How do I use this?
|
How do I use this?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.showHelp}>
|
<Show when={showHelp()}>
|
||||||
<div class="help-panel">
|
<div class="help-panel">
|
||||||
<h4>Generating Stats</h4>
|
<h4>Generating Stats</h4>
|
||||||
<code>NIX_SHOW_STATS=1 nix-instantiate expr.nix > stats.json</code>
|
<code>NIX_SHOW_STATS=1 nix-instantiate expr.nix > stats.json</code>
|
||||||
|
|
@ -85,6 +84,17 @@ export default function FileUpload(props: FileUploadProps) {
|
||||||
class="json-input"
|
class="json-input"
|
||||||
value={textInput()}
|
value={textInput()}
|
||||||
onInput={e => setTextInput(e.currentTarget.value)}
|
onInput={e => setTextInput(e.currentTarget.value)}
|
||||||
|
onPaste={e => {
|
||||||
|
const text = e.clipboardData?.getData('text');
|
||||||
|
if (!text) return;
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
JSON.parse(text);
|
||||||
|
props.onTextLoad(text);
|
||||||
|
} catch {
|
||||||
|
setError('Invalid JSON in clipboard');
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button class="load-btn" onClick={handleTextLoad}>
|
<button class="load-btn" onClick={handleTextLoad}>
|
||||||
Load
|
Load
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,12 @@ function App() {
|
||||||
const [view, setView] = createSignal<'analysis' | 'compare'>('analysis');
|
const [view, setView] = createSignal<'analysis' | 'compare'>('analysis');
|
||||||
const [snapshotName, setSnapshotName] = createSignal('');
|
const [snapshotName, setSnapshotName] = createSignal('');
|
||||||
const [showSaveDialog, setShowSaveDialog] = createSignal(false);
|
const [showSaveDialog, setShowSaveDialog] = createSignal(false);
|
||||||
const [showHelp, setShowHelp] = createSignal(false);
|
|
||||||
const [showManageSnapshots, setShowManageSnapshots] = createSignal(false);
|
const [showManageSnapshots, setShowManageSnapshots] = createSignal(false);
|
||||||
const [precision, setPrecision] = createSignal(2);
|
const [precision, setPrecision] = createSignal(2);
|
||||||
const [pasteMode, setPasteMode] = createSignal<'advance' | 'replace'>('advance');
|
const [pasteMode, setPasteMode] = createSignal<'advance' | 'replace'>('advance');
|
||||||
const [isLoading, setIsLoading] = createSignal(true);
|
const [isLoading, setIsLoading] = createSignal(true);
|
||||||
|
const [showOverrideModal, setShowOverrideModal] = createSignal(false);
|
||||||
|
const [pendingOverrideText, setPendingOverrideText] = createSignal('');
|
||||||
|
|
||||||
const STORAGE_KEY = 'ns-data';
|
const STORAGE_KEY = 'ns-data';
|
||||||
|
|
||||||
|
|
@ -80,8 +81,28 @@ function App() {
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
const handleAnalysisPaste = (e: ClipboardEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = e.clipboardData?.getData('text');
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (view() === 'analysis' && currentStats()) {
|
||||||
|
setPendingOverrideText(text);
|
||||||
|
setShowOverrideModal(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('paste', handleAnalysisPaste);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.removeEventListener('paste', handleAnalysisPaste);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -199,6 +220,34 @@ function App() {
|
||||||
return entry;
|
return entry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCompareFileLoad = (data: StatsData, raw: Record<string, unknown>) => {
|
||||||
|
const entry: ComparisonEntry = {
|
||||||
|
id: Date.now(),
|
||||||
|
name: `Snapshot ${snapshots().length + 1}`,
|
||||||
|
data,
|
||||||
|
raw,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
setSnapshots(prev => [...prev, entry]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompareTextLoad = (text: string) => {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(text);
|
||||||
|
const data = parseStats(raw);
|
||||||
|
const entry: ComparisonEntry = {
|
||||||
|
id: Date.now(),
|
||||||
|
name: `Snapshot ${snapshots().length + 1}`,
|
||||||
|
data,
|
||||||
|
raw,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
setSnapshots(prev => [...prev, entry]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse stats:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
|
@ -209,11 +258,7 @@ function App() {
|
||||||
<button class={view() === 'analysis' ? 'active' : ''} onClick={() => setView('analysis')}>
|
<button class={view() === 'analysis' ? 'active' : ''} onClick={() => setView('analysis')}>
|
||||||
Analysis
|
Analysis
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class={view() === 'compare' ? 'active' : ''} onClick={() => setView('compare')}>
|
||||||
class={view() === 'compare' ? 'active' : ''}
|
|
||||||
onClick={() => setView('compare')}
|
|
||||||
disabled={snapshots().length < 2}
|
|
||||||
>
|
|
||||||
Compare ({snapshots().length})
|
Compare ({snapshots().length})
|
||||||
</button>
|
</button>
|
||||||
<Show when={snapshots().length > 0}>
|
<Show when={snapshots().length > 0}>
|
||||||
|
|
@ -251,8 +296,6 @@ function App() {
|
||||||
<FileUpload
|
<FileUpload
|
||||||
onFileLoad={handleFileLoad}
|
onFileLoad={handleFileLoad}
|
||||||
onTextLoad={loadFromText}
|
onTextLoad={loadFromText}
|
||||||
showHelp={showHelp()}
|
|
||||||
onToggleHelp={() => setShowHelp(!showHelp())}
|
|
||||||
snapshots={snapshots()}
|
snapshots={snapshots()}
|
||||||
onLoadSnapshot={loadSnapshot}
|
onLoadSnapshot={loadSnapshot}
|
||||||
/>
|
/>
|
||||||
|
|
@ -283,6 +326,30 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={showOverrideModal()}>
|
||||||
|
<div class="modal-overlay" onClick={() => setShowOverrideModal(false)}>
|
||||||
|
<div class="modal" onClick={e => e.stopPropagation()}>
|
||||||
|
<h3>Override Analysis?</h3>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', 'margin-bottom': '1rem' }}>
|
||||||
|
This will replace the current analysis with pasted data.
|
||||||
|
</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="cancel-btn" onClick={() => setShowOverrideModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="confirm-btn"
|
||||||
|
onClick={() => {
|
||||||
|
loadFromText(pendingOverrideText());
|
||||||
|
setShowOverrideModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Override
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|
@ -295,6 +362,8 @@ function App() {
|
||||||
pasteMode={pasteMode()}
|
pasteMode={pasteMode()}
|
||||||
onPasteModeChange={setPasteMode}
|
onPasteModeChange={setPasteMode}
|
||||||
onPasteStats={handlePasteStats}
|
onPasteStats={handlePasteStats}
|
||||||
|
onFileLoad={handleCompareFileLoad}
|
||||||
|
onTextLoad={handleCompareTextLoad}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,7 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
padding-top: 4rem;
|
padding-top: 4rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-card {
|
.upload-card {
|
||||||
|
|
@ -1504,6 +1505,25 @@ body {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compare-placeholder-hint {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-upload-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-more-needed {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.comparison-table {
|
.comparison-table {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue