First commit for bicorder-app

This commit is contained in:
Nathan Schneider
2025-11-25 13:20:21 -05:00
parent 3a55d3dbb9
commit b541f6049e
24 changed files with 8883 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { BicorderState, Gradient, AnalysisGradient } from './types';
import GradientSlider from './components/GradientSlider.svelte';
import MetadataFields from './components/MetadataFields.svelte';
import AnalysisDisplay from './components/AnalysisDisplay.svelte';
import ExportControls from './components/ExportControls.svelte';
// Load bicorder data from build-time constant
let data: BicorderState = JSON.parse(JSON.stringify(__BICORDER_DATA__));
// Initialize timestamp if not set
if (!data.metadata.timestamp) {
data.metadata.timestamp = new Date().toISOString();
}
// Load saved state from localStorage
onMount(() => {
const saved = localStorage.getItem('bicorder-state');
if (saved) {
try {
const savedData = JSON.parse(saved);
// Preserve the structure but update values
data.metadata = { ...data.metadata, ...savedData.metadata };
// Update gradient values
data.diagnostic.forEach((set, setIdx) => {
set.gradients.forEach((gradient, gradIdx) => {
const savedGradient = savedData.diagnostic?.[setIdx]?.gradients?.[gradIdx];
if (savedGradient) {
gradient.value = savedGradient.value;
gradient.notes = savedGradient.notes;
}
});
});
// Update analysis values
data.analysis.forEach((item, idx) => {
const savedItem = savedData.analysis?.[idx];
if (savedItem) {
item.notes = savedItem.notes;
}
});
} catch (e) {
console.error('Failed to load saved state:', e);
}
}
});
// Save state whenever it changes
$: {
if (typeof window !== 'undefined') {
localStorage.setItem('bicorder-state', JSON.stringify(data));
}
}
// Auto-calculate analysis values
function calculateHardness(): number | null {
const values = data.diagnostic
.flatMap(set => set.gradients)
.filter(g => !data.metadata.shortform || g.shortform)
.map(g => g.value)
.filter((v): v is number => v !== null);
if (values.length === 0) return null;
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
return Math.round(mean);
}
function calculatePolarization(): number | null {
const values = data.diagnostic
.flatMap(set => set.gradients)
.filter(g => !data.metadata.shortform || g.shortform)
.map(g => g.value)
.filter((v): v is number => v !== null);
if (values.length === 0) return null;
// Calculate how far values are from center (5)
const deviations = values.map(v => Math.abs(v - 5));
const avgDeviation = deviations.reduce((sum, d) => sum + d, 0) / deviations.length;
// Map deviation to polarized (1) vs centrist (9) scale
// Max deviation is 4 (from 1 or 9), min is 0 (at 5)
// Higher deviation = more polarized = lower value
const polarizationScore = 9 - (avgDeviation / 4) * 8;
return Math.round(polarizationScore);
}
// Update automated analysis values reactively
$: {
data.analysis.forEach((item, index) => {
if (item.automated) {
if (index === 0) {
data.analysis[0].value = calculateHardness();
} else if (index === 1) {
data.analysis[1].value = calculatePolarization();
}
}
});
}
function handleMetadataUpdate(event: CustomEvent) {
data.metadata = { ...data.metadata, ...event.detail };
}
function handleReset() {
if (confirm('Reset all values? This cannot be undone.')) {
localStorage.removeItem('bicorder-state');
location.reload();
}
}
</script>
<main>
<div class="header">
<div class="title">Protocol</div>
<div class="title">BICORDER</div>
</div>
<MetadataFields
metadata={data.metadata}
on:update={handleMetadataUpdate}
/>
{#each data.diagnostic as diagnosticSet, setIndex}
<section class="diagnostic-set">
<div class="set-header">{diagnosticSet.set_name.toUpperCase()}</div>
<div class="set-description">{diagnosticSet.set_description}</div>
{#each diagnosticSet.gradients as gradient, gradientIndex}
{#if !data.metadata.shortform || gradient.shortform}
<GradientSlider
{gradient}
on:change={(e) => {
data.diagnostic[setIndex].gradients[gradientIndex].value = e.detail ?? null;
data = data;
}}
on:notes={(e) => {
data.diagnostic[setIndex].gradients[gradientIndex].notes = e.detail;
data = data;
}}
/>
{/if}
{/each}
</section>
{/each}
<section class="analysis-section">
<div class="set-header">ANALYSIS</div>
{#each data.analysis as analysisItem, index}
<AnalysisDisplay
gradient={analysisItem}
on:change={(e) => {
if (!analysisItem.automated) {
data.analysis[index].value = e.detail ?? null;
data = data;
}
}}
on:notes={(e) => {
data.analysis[index].notes = e.detail;
data = data;
}}
/>
{/each}
</section>
<ExportControls {data} on:reset={handleReset} />
</main>
<style>
main {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
border-bottom: 2px solid var(--border-color);
padding-bottom: 1rem;
}
.title {
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 0.2rem;
}
.diagnostic-set, .analysis-section {
margin: 2rem 0;
padding: 1rem 0;
border-top: 2px solid var(--border-color);
}
.set-header {
text-align: center;
font-size: 1.2rem;
font-weight: bold;
letter-spacing: 0.2rem;
margin-bottom: 0.5rem;
}
.set-description {
text-align: center;
font-size: 0.9rem;
margin-bottom: 1.5rem;
opacity: 0.8;
}
@media (max-width: 768px) {
main {
padding: 0.5rem;
}
.title {
font-size: 1.2rem;
}
.set-header {
font-size: 1rem;
}
}
</style>