test: Created OutputInfoBar with color-coded stats, wired Use This butt…
- "app/src/components/OutputInfoBar.tsx" - "app/src/components/__tests__/OutputInfoBar.test.tsx" - "app/src/views/ImportConvert.tsx" - "app/src/App.css" GSD-Task: S01/T04
This commit is contained in:
parent
c3783e1680
commit
383825e242
4 changed files with 237 additions and 9 deletions
|
|
@ -324,6 +324,77 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Output Info Bar */
|
||||||
|
.output-info-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-bar--empty {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-placeholder {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--green .output-stat-value {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--yellow .output-stat-value {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--red .output-stat-value {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-warnings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-warning {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
/* Placeholder views */
|
/* Placeholder views */
|
||||||
.placeholder-view {
|
.placeholder-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
68
app/src/components/OutputInfoBar.tsx
Normal file
68
app/src/components/OutputInfoBar.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { TraceMetadata } from '../types/engine';
|
||||||
|
|
||||||
|
interface OutputInfoBarProps {
|
||||||
|
metadata: TraceMetadata | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatColor = 'green' | 'yellow' | 'red';
|
||||||
|
|
||||||
|
function getNodeCountColor(count: number): StatColor {
|
||||||
|
if (count > 5000) return 'yellow';
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenPathsColor(count: number): StatColor {
|
||||||
|
if (count > 0) return 'red';
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OutputInfoBar({ metadata }: OutputInfoBarProps) {
|
||||||
|
if (!metadata) {
|
||||||
|
return (
|
||||||
|
<div className="output-info-bar output-info-bar--empty">
|
||||||
|
<span className="output-info-placeholder">
|
||||||
|
Trace an image to see output stats
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeColor = getNodeCountColor(metadata.node_count_total);
|
||||||
|
const openPathsColor = getOpenPathsColor(metadata.open_paths);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="output-info-bar">
|
||||||
|
<div className="output-info-stats">
|
||||||
|
<span className="output-stat output-stat--green" data-testid="stat-paths">
|
||||||
|
<span className="output-stat-label">Paths</span>
|
||||||
|
<span className="output-stat-value">{metadata.path_count}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`output-stat output-stat--${nodeColor}`}
|
||||||
|
data-testid="stat-nodes"
|
||||||
|
>
|
||||||
|
<span className="output-stat-label">Nodes</span>
|
||||||
|
<span className="output-stat-value">{metadata.node_count_total.toLocaleString()}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`output-stat output-stat--${openPathsColor}`}
|
||||||
|
data-testid="stat-open-paths"
|
||||||
|
>
|
||||||
|
<span className="output-stat-label">Open Paths</span>
|
||||||
|
<span className="output-stat-value">{metadata.open_paths}</span>
|
||||||
|
</span>
|
||||||
|
<span className="output-stat output-stat--green" data-testid="stat-time">
|
||||||
|
<span className="output-stat-label">Time</span>
|
||||||
|
<span className="output-stat-value">{metadata.processing_ms}ms</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{metadata.warnings.length > 0 && (
|
||||||
|
<div className="output-info-warnings" data-testid="warnings">
|
||||||
|
{metadata.warnings.map((w, i) => (
|
||||||
|
<span key={i} className="output-info-warning">⚠ {w}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
app/src/components/__tests__/OutputInfoBar.test.tsx
Normal file
88
app/src/components/__tests__/OutputInfoBar.test.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import OutputInfoBar from '../OutputInfoBar';
|
||||||
|
import type { TraceMetadata } from '../../types/engine';
|
||||||
|
|
||||||
|
function makeMetadata(overrides: Partial<TraceMetadata> = {}): TraceMetadata {
|
||||||
|
return {
|
||||||
|
format: 'svg',
|
||||||
|
path_count: 42,
|
||||||
|
node_count_total: 1200,
|
||||||
|
open_paths: 0,
|
||||||
|
island_count: 3,
|
||||||
|
warnings: [],
|
||||||
|
processing_ms: 150,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OutputInfoBar', () => {
|
||||||
|
it('shows placeholder when metadata is null', () => {
|
||||||
|
const { container } = render(<OutputInfoBar metadata={null} />);
|
||||||
|
expect(container.querySelector('.output-info-bar--empty')).toBeTruthy();
|
||||||
|
expect(screen.getByText(/trace an image/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders stats with green indicators for normal metadata', () => {
|
||||||
|
const meta = makeMetadata();
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const pathsStat = screen.getByTestId('stat-paths');
|
||||||
|
expect(pathsStat).toHaveClass('output-stat--green');
|
||||||
|
expect(pathsStat).toHaveTextContent('42');
|
||||||
|
|
||||||
|
const nodesStat = screen.getByTestId('stat-nodes');
|
||||||
|
expect(nodesStat).toHaveClass('output-stat--green');
|
||||||
|
expect(nodesStat).toHaveTextContent('1,200');
|
||||||
|
|
||||||
|
const openStat = screen.getByTestId('stat-open-paths');
|
||||||
|
expect(openStat).toHaveClass('output-stat--green');
|
||||||
|
expect(openStat).toHaveTextContent('0');
|
||||||
|
|
||||||
|
const timeStat = screen.getByTestId('stat-time');
|
||||||
|
expect(timeStat).toHaveClass('output-stat--green');
|
||||||
|
expect(timeStat).toHaveTextContent('150ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows yellow indicator when node_count_total > 5000', () => {
|
||||||
|
const meta = makeMetadata({ node_count_total: 8500 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const nodesStat = screen.getByTestId('stat-nodes');
|
||||||
|
expect(nodesStat).toHaveClass('output-stat--yellow');
|
||||||
|
expect(nodesStat).toHaveTextContent('8,500');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows red indicator when open_paths > 0', () => {
|
||||||
|
const meta = makeMetadata({ open_paths: 3 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const openStat = screen.getByTestId('stat-open-paths');
|
||||||
|
expect(openStat).toHaveClass('output-stat--red');
|
||||||
|
expect(openStat).toHaveTextContent('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows both yellow and red indicators simultaneously', () => {
|
||||||
|
const meta = makeMetadata({ node_count_total: 6000, open_paths: 5 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('stat-nodes')).toHaveClass('output-stat--yellow');
|
||||||
|
expect(screen.getByTestId('stat-open-paths')).toHaveClass('output-stat--red');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays warnings when present', () => {
|
||||||
|
const meta = makeMetadata({ warnings: ['Too many paths', 'Complex geometry'] });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const warnings = screen.getByTestId('warnings');
|
||||||
|
expect(warnings).toHaveTextContent('Too many paths');
|
||||||
|
expect(warnings).toHaveTextContent('Complex geometry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render warnings container when no warnings', () => {
|
||||||
|
const meta = makeMetadata({ warnings: [] });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('warnings')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,7 @@ import FileUpload from '../components/FileUpload';
|
||||||
import PresetSelector from '../components/PresetSelector';
|
import PresetSelector from '../components/PresetSelector';
|
||||||
import ParameterSliders from '../components/ParameterSliders';
|
import ParameterSliders from '../components/ParameterSliders';
|
||||||
import SvgPreview from '../components/SvgPreview';
|
import SvgPreview from '../components/SvgPreview';
|
||||||
|
import OutputInfoBar from '../components/OutputInfoBar';
|
||||||
import { useDebouncedTrace } from '../hooks/useDebouncedTrace';
|
import { useDebouncedTrace } from '../hooks/useDebouncedTrace';
|
||||||
import styles from './ImportConvert.module.css';
|
import styles from './ImportConvert.module.css';
|
||||||
|
|
||||||
|
|
@ -93,15 +94,14 @@ export default function ImportConvert({ onUseThis }: ImportConvertProps) {
|
||||||
params={currentParams}
|
params={currentParams}
|
||||||
onChange={handleParamsChange}
|
onChange={handleParamsChange}
|
||||||
/>
|
/>
|
||||||
{svgOutput && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="use-this-btn"
|
||||||
className="use-this-btn"
|
disabled={!svgOutput || isLoading}
|
||||||
onClick={handleUseThis}
|
onClick={handleUseThis}
|
||||||
>
|
>
|
||||||
Use This →
|
Use This →
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightPanel}>
|
<div className={styles.rightPanel}>
|
||||||
<SvgPreview
|
<SvgPreview
|
||||||
|
|
@ -110,6 +110,7 @@ export default function ImportConvert({ onUseThis }: ImportConvertProps) {
|
||||||
error={error}
|
error={error}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
/>
|
/>
|
||||||
|
<OutputInfoBar metadata={metadata} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue