First commit for bicorder-app
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user