Added classifer analysis to bicorder ascii and web app
This commit is contained in:
+223
-46
@@ -6,9 +6,15 @@
|
||||
import AnalysisDisplay from './components/AnalysisDisplay.svelte';
|
||||
import ExportControls from './components/ExportControls.svelte';
|
||||
import HelpModal from './components/HelpModal.svelte';
|
||||
import FormRecommendation from './components/FormRecommendation.svelte';
|
||||
import { BicorderClassifier } from './bicorder-classifier';
|
||||
|
||||
// Load bicorder data from build-time constant
|
||||
// Load bicorder data and model from build-time constants
|
||||
let data: BicorderState = JSON.parse(JSON.stringify(__BICORDER_DATA__));
|
||||
const model = __BICORDER_MODEL__;
|
||||
|
||||
// Initialize classifier
|
||||
const classifier = new BicorderClassifier(model, data.version);
|
||||
|
||||
// Initialize timestamp if not set
|
||||
if (!data.metadata.timestamp) {
|
||||
@@ -30,7 +36,7 @@
|
||||
| { type: 'export' };
|
||||
|
||||
// Calculate all screens based on current shortform setting
|
||||
function calculateScreens(): Screen[] {
|
||||
function calculateScreens(isShortForm: boolean): Screen[] {
|
||||
const screens: Screen[] = [];
|
||||
|
||||
// Metadata screen
|
||||
@@ -39,7 +45,7 @@
|
||||
// Diagnostic gradient screens
|
||||
data.diagnostic.forEach((diagnosticSet, setIndex) => {
|
||||
diagnosticSet.gradients.forEach((gradient, gradientIndex) => {
|
||||
if (!data.metadata.shortform || gradient.shortform) {
|
||||
if (!isShortForm || gradient.shortform) {
|
||||
screens.push({
|
||||
type: 'gradient',
|
||||
setIndex,
|
||||
@@ -52,7 +58,7 @@
|
||||
});
|
||||
|
||||
// Analysis screens (not in shortform)
|
||||
if (!data.metadata.shortform) {
|
||||
if (!isShortForm) {
|
||||
data.analysis.forEach((gradient, index) => {
|
||||
screens.push({ type: 'analysis', index, gradient });
|
||||
});
|
||||
@@ -64,10 +70,16 @@
|
||||
return screens;
|
||||
}
|
||||
|
||||
$: screens = calculateScreens();
|
||||
// Recalculate screens when data or shortform changes (explicit dependency)
|
||||
$: screens = calculateScreens(data.metadata.shortform);
|
||||
$: currentScreenData = screens[currentScreen];
|
||||
$: totalScreens = screens.length;
|
||||
|
||||
// Debug: log when screens change
|
||||
$: if (screens) {
|
||||
console.log(`Screens updated: ${screens.length} total, shortform: ${data.metadata.shortform}`);
|
||||
}
|
||||
|
||||
function goToNextScreen() {
|
||||
if (currentScreen < totalScreens - 1) {
|
||||
currentScreen++;
|
||||
@@ -188,7 +200,59 @@
|
||||
// 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);
|
||||
|
||||
// Clamp to 1-9 range and round
|
||||
return Math.round(Math.max(1, Math.min(9, polarizationScore)));
|
||||
}
|
||||
|
||||
function ldaScoreToScale(ldaScore: number | null): number | null {
|
||||
/**
|
||||
* Convert LDA score to 1-9 scale.
|
||||
* LDA scores typically range from -4 to +4 (8 range)
|
||||
* Target scale is 1 to 9 (8 range)
|
||||
*
|
||||
* Formula: value = 5 + (ldaScore * 4/3)
|
||||
* - LDA -3 or less → 1 (bureaucratic)
|
||||
* - LDA 0 → 5 (boundary)
|
||||
* - LDA +3 or more → 9 (relational)
|
||||
*/
|
||||
if (ldaScore === null) return null;
|
||||
|
||||
// Scale: value = 5 + (ldaScore * 1.33)
|
||||
const value = 5 + (ldaScore * 4.0 / 3.0);
|
||||
|
||||
// Clamp to 1-9 range and round
|
||||
return Math.round(Math.max(1, Math.min(9, value)));
|
||||
}
|
||||
|
||||
function calculateBureaucratic(): number | null {
|
||||
// Collect all diagnostic gradients with their set and gradient info
|
||||
const ratings: Record<string, number> = {};
|
||||
|
||||
data.diagnostic.forEach((diagnosticSet) => {
|
||||
const setName = diagnosticSet.set_name;
|
||||
diagnosticSet.gradients.forEach((gradient) => {
|
||||
if (gradient.value !== null) {
|
||||
// Create dimension name in format: SetName_left_vs_right
|
||||
const dimensionName = `${setName}_${gradient.term_left}_vs_${gradient.term_right}`;
|
||||
ratings[dimensionName] = gradient.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check if we have any ratings
|
||||
if (Object.keys(ratings).length === 0) return null;
|
||||
|
||||
try {
|
||||
// Get prediction from classifier (need detailed: true to get ldaScore)
|
||||
const result = classifier.predict(ratings, { detailed: true });
|
||||
|
||||
// Convert LDA score to 1-9 scale
|
||||
return ldaScoreToScale(result.ldaScore);
|
||||
} catch (error) {
|
||||
console.error('Error calculating bureaucratic/relational score:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update automated analysis values reactively
|
||||
@@ -196,16 +260,29 @@
|
||||
data.analysis.forEach((item, index) => {
|
||||
if (item.automated) {
|
||||
if (index === 0) {
|
||||
// Hardness/Softness
|
||||
data.analysis[0].value = calculateHardness();
|
||||
} else if (index === 1) {
|
||||
// Polarized/Centrist
|
||||
data.analysis[1].value = calculatePolarization();
|
||||
} else if (index === 2) {
|
||||
// Bureaucratic/Relational (LDA classifier)
|
||||
data.analysis[2].value = calculateBureaucratic();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMetadataUpdate(event: CustomEvent) {
|
||||
data.metadata = { ...data.metadata, ...event.detail };
|
||||
// Properly trigger reactivity for nested metadata changes
|
||||
data = {
|
||||
...data,
|
||||
metadata: { ...data.metadata, ...event.detail }
|
||||
};
|
||||
// Force refresh if shortform changed
|
||||
if (event.detail.shortform !== undefined) {
|
||||
refreshKey++;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
@@ -215,21 +292,79 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressBarClick(event: MouseEvent) {
|
||||
// Calculate which screen to jump to based on click position
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const width = rect.width;
|
||||
|
||||
// Calculate ratio (0 to 1)
|
||||
const ratio = Math.max(0, Math.min(1, x / width));
|
||||
|
||||
// Calculate target screen (round to nearest)
|
||||
const targetScreen = Math.round(ratio * (totalScreens - 1));
|
||||
|
||||
// Jump to that screen
|
||||
currentScreen = targetScreen;
|
||||
}
|
||||
|
||||
function openHelp() {
|
||||
isHelpOpen = true;
|
||||
}
|
||||
|
||||
function handleSwitchToLongForm() {
|
||||
// Turn off shortform mode while preserving all entered values
|
||||
data = {
|
||||
...data,
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
shortform: false
|
||||
}
|
||||
};
|
||||
// Force refresh of components
|
||||
refreshKey++;
|
||||
// Reset to first screen to show user the full form
|
||||
currentScreen = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<HelpModal bind:isOpen={isHelpOpen} />
|
||||
|
||||
<main>
|
||||
<div class="header">
|
||||
<button class="help-btn" on:click={openHelp} aria-label="About the Bicorder">?</button>
|
||||
<div class="title">Protocol</div>
|
||||
<div class="title">BICORDER</div>
|
||||
<button class="mode-toggle" on:click={toggleViewMode} aria-label="Toggle view mode">
|
||||
{viewMode === 'focused' ? '☰' : '⊡'}
|
||||
</button>
|
||||
<div class="header-left">
|
||||
<button
|
||||
class="toolbar-btn help-btn"
|
||||
on:click={openHelp}
|
||||
aria-label="About the Bicorder"
|
||||
title="Help & Instructions"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<div class="title">Protocol</div>
|
||||
<div class="title">BICORDER</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<FormRecommendation
|
||||
{classifier}
|
||||
diagnosticData={data.diagnostic}
|
||||
isShortForm={data.metadata.shortform}
|
||||
on:switchToLongForm={handleSwitchToLongForm}
|
||||
/>
|
||||
<button
|
||||
class="toolbar-btn mode-toggle"
|
||||
on:click={toggleViewMode}
|
||||
aria-label="Toggle view mode"
|
||||
title={viewMode === 'focused' ? 'Switch to list view' : 'Switch to focused view'}
|
||||
>
|
||||
{viewMode === 'focused' ? '☰' : '⊡'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if viewMode === 'list'}
|
||||
@@ -407,7 +542,18 @@
|
||||
</div>
|
||||
|
||||
<div class="progress-indicator">
|
||||
<div class="progress-bar">{progressBar}</div>
|
||||
<div
|
||||
class="progress-bar clickable"
|
||||
on:click={handleProgressBarClick}
|
||||
role="slider"
|
||||
aria-label="Jump to screen"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={totalScreens - 1}
|
||||
aria-valuenow={currentScreen}
|
||||
title="Click to jump to a screen"
|
||||
>
|
||||
{progressBar}
|
||||
</div>
|
||||
<div class="progress-numbers">{currentScreen + 1} / {totalScreens}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,11 +575,31 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding-bottom: 1rem;
|
||||
position: relative;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -442,51 +608,37 @@
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.help-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.toolbar-btn {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s, background-color 0.2s;
|
||||
min-height: auto;
|
||||
flex-shrink: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.help-btn:hover {
|
||||
.toolbar-btn.help-btn {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
opacity: 0.8;
|
||||
background-color: var(--input-bg);
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
background: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
min-height: auto;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.mode-toggle:hover {
|
||||
opacity: 0.8;
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -657,6 +809,23 @@
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.progress-bar.clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar.clickable:hover {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.progress-bar.clickable:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.progress-numbers {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
@@ -669,6 +838,11 @@
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
@@ -677,15 +851,18 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.help-btn {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
font-size: 1rem;
|
||||
.toolbar-btn {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.toolbar-btn.help-btn {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
Reference in New Issue
Block a user