Added classifer analysis to bicorder ascii and web app
This commit is contained in:
450
bicorder-app/src/components/FormRecommendation.svelte
Normal file
450
bicorder-app/src/components/FormRecommendation.svelte
Normal file
@@ -0,0 +1,450 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { BicorderClassifier } from '../bicorder-classifier';
|
||||
|
||||
export let classifier: BicorderClassifier;
|
||||
export let diagnosticData: any;
|
||||
export let isShortForm: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
switchToLongForm: void;
|
||||
}>();
|
||||
|
||||
let isExpanded = false;
|
||||
let recommendation: any = null;
|
||||
let hasEnoughData = false;
|
||||
|
||||
// Calculate recommendation based on current diagnostic data
|
||||
$: {
|
||||
// Collect ratings from diagnostic data
|
||||
const ratings: Record<string, number> = {};
|
||||
let valueCount = 0;
|
||||
let shortFormTotal = 0;
|
||||
|
||||
diagnosticData.forEach((diagnosticSet: any) => {
|
||||
const setName = diagnosticSet.set_name;
|
||||
diagnosticSet.gradients.forEach((gradient: any) => {
|
||||
// Count shortform gradients
|
||||
if (gradient.shortform) {
|
||||
shortFormTotal++;
|
||||
}
|
||||
|
||||
if (gradient.value !== null) {
|
||||
const dimensionName = `${setName}_${gradient.term_left}_vs_${gradient.term_right}`;
|
||||
ratings[dimensionName] = gradient.value;
|
||||
|
||||
// Only count shortform values for the threshold
|
||||
if (gradient.shortform) {
|
||||
valueCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Only show if at least half of shortform gradients are complete
|
||||
const threshold = Math.ceil(shortFormTotal / 2);
|
||||
hasEnoughData = valueCount >= threshold;
|
||||
|
||||
if (hasEnoughData && isShortForm) {
|
||||
try {
|
||||
const prediction = classifier.predict(ratings, { detailed: true });
|
||||
const assessment = classifier.assessShortFormReadiness(ratings);
|
||||
recommendation = {
|
||||
...prediction,
|
||||
...assessment,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting form recommendation:', error);
|
||||
recommendation = null;
|
||||
}
|
||||
} else {
|
||||
recommendation = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
|
||||
function handleSwitchToLongForm() {
|
||||
dispatch('switchToLongForm');
|
||||
isExpanded = false;
|
||||
}
|
||||
|
||||
// Determine status: 'good' (green) or 'warning' (yellow/orange)
|
||||
$: status = recommendation?.recommendedForm === 'long' ? 'warning' : 'good';
|
||||
$: showIndicator = hasEnoughData && isShortForm && recommendation;
|
||||
</script>
|
||||
|
||||
{#if showIndicator}
|
||||
<div class="form-recommendation" class:expanded={isExpanded}>
|
||||
<button
|
||||
class="indicator"
|
||||
class:good={status === 'good'}
|
||||
class:warning={status === 'warning'}
|
||||
on:click={toggleExpanded}
|
||||
aria-label="Data quality indicator"
|
||||
title={status === 'good' ? 'Short form working well' : 'Long form recommended'}
|
||||
>
|
||||
<span class="light"></span>
|
||||
</button>
|
||||
|
||||
{#if isExpanded}
|
||||
<div class="panel-backdrop" on:click={toggleExpanded} on:keydown={() => {}} role="button" tabindex="-1">
|
||||
<div class="details-panel" on:click|stopPropagation on:keydown={() => {}} role="dialog" aria-modal="true">
|
||||
<div class="panel-header">
|
||||
<h3>Data Quality Assessment</h3>
|
||||
<button class="close-btn" on:click={toggleExpanded} aria-label="Close">+</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Classification Confidence:</span>
|
||||
<span class="metric-value" class:low={recommendation.confidence < 60}>
|
||||
{recommendation.confidence}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="metric-label">Data Completeness:</span>
|
||||
<span class="metric-value" class:low={recommendation.completeness < 50}>
|
||||
{recommendation.completeness}% ({recommendation.dimensionsProvided}/{recommendation.dimensionsTotal} dimensions)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="metric-label">Key Dimensions:</span>
|
||||
<span class="metric-value" class:low={recommendation.coverage < 75}>
|
||||
{recommendation.coverage}% ({recommendation.keyDimensionsProvided}/{recommendation.keyDimensionsTotal})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="classification">
|
||||
<div class="classification-label">Current Classification:</div>
|
||||
<div class="classification-value">
|
||||
<strong>{recommendation.clusterName}</strong>
|
||||
{#if recommendation.distanceToBoundary < 0.5}
|
||||
<span class="boundary-warning">(Near boundary)</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if recommendation.recommendedForm === 'long'}
|
||||
<div class="recommendation-message warning">
|
||||
<strong>⚠ Long Form Recommended</strong>
|
||||
<p>
|
||||
{#if recommendation.confidence < 60}
|
||||
• Low classification confidence<br>
|
||||
{/if}
|
||||
{#if recommendation.completeness < 50}
|
||||
• Incomplete data (less than 50% of dimensions)<br>
|
||||
{/if}
|
||||
{#if recommendation.distanceToBoundary < 0.5}
|
||||
• Protocol near boundary between families<br>
|
||||
{/if}
|
||||
{#if recommendation.coverage < 75}
|
||||
• Missing key dimensions for reliable short-form classification<br>
|
||||
{/if}
|
||||
</p>
|
||||
<button class="switch-btn" on:click={handleSwitchToLongForm}>
|
||||
Switch to Long Form →
|
||||
</button>
|
||||
<p class="note">All your current values will be preserved.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="recommendation-message good">
|
||||
<strong>✓ Short Form Working Well</strong>
|
||||
<p>
|
||||
Your current data provides {recommendation.confidence}% confidence classification.
|
||||
Continue with short form or switch to long form for more detailed analysis.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.form-recommendation {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0;
|
||||
opacity: 0.4;
|
||||
min-height: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.indicator:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.light {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator.good .light {
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 8px rgba(74, 222, 128, 0.5);
|
||||
}
|
||||
|
||||
.indicator.warning .light {
|
||||
background: #fbbf24;
|
||||
box-shadow: 0 0 8px rgba(251, 191, 36, 0.5);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-backdrop {
|
||||
/* Hidden on desktop - only visible on mobile */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
background: var(--bg-color);
|
||||
border: 2px solid var(--border-color);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
animation: slideIn 0.2s ease-out;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--fg-color);
|
||||
opacity: 0.6;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 1rem;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: bold;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.metric-value.low {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.classification {
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.classification-label {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.classification-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.classification-value strong {
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.boundary-warning {
|
||||
color: #fbbf24;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.recommendation-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.recommendation-message.good {
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
border-color: #4ade80;
|
||||
}
|
||||
|
||||
.recommendation-message.warning {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
.recommendation-message strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.recommendation-message p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
background: #fbbf24;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.switch-btn:hover {
|
||||
background: #f59e0b;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.indicator {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.light {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
/* Modal-like on mobile */
|
||||
.panel-backdrop {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user