feat(stage-tamagotchi): settings - modules - hearing panel

This commit is contained in:
Neko Ayaka
2025-07-02 03:41:31 +08:00
parent fd09558e59
commit 91364ea305
6 changed files with 549 additions and 35 deletions
@@ -14,7 +14,7 @@ pub fn new_settings_window<R: Runtime>(
WebviewUrl::App(Path::new("#/settings").to_path_buf()),
)
.title("Settings")
.inner_size(450.0, 800.0)
.inner_size(550.0, 900.0)
.shadow(true)
.transparent(false)
.accept_first_mouse(true);
@@ -1,6 +1,520 @@
<script setup lang="ts">
import { LevelMeter, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
import { FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
import { useDevicesList } from '@vueuse/core'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useTauriCore } from '../../../composables/tauri'
const devices = useDevicesList({ constraints: { audio: true } })
const audioInputs = computed(() => devices.audioInputs.value)
const { invoke } = useTauriCore()
const selectedAudioInput = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
const isMonitoring = ref(false)
const enablePlayback = ref(false)
// Audio processing state
const audioContext = ref<AudioContext>()
const mediaStream = ref<MediaStream>()
const analyser = ref<AnalyserNode>()
const gainNode = ref<GainNode>()
const dataArray = ref<Uint8Array>()
const animationFrame = ref<number>()
// Audio levels and indicators
const volumeLevel = ref(0) // 0-100
const isSpeaking = ref(false)
const speakingThreshold = ref(25) // 0-100 (for volume-based fallback)
const monitorVolume = ref(50) // 0-100
// Tauri VAD integration
const isVADModelLoaded = ref(false)
const isLoadingVADModel = ref(false)
const vadModelError = ref('')
const useVADModel = ref(true) // Toggle between Tauri VAD and volume-based detection
const vadProbability = ref(0) // Raw VAD probability from Tauri
const vadThreshold = ref(0.5) // VAD probability threshold for speech detection
// Audio chunk buffering for Tauri VAD
const audioChunkBuffer = ref<Float32Array | null>(null)
const chunkSize = 512 // 32ms at 16kHz
const vadProcessingInterval = ref<number | null>(null)
// VAD visualization
const vadHistory = ref<number[]>([]) // History for chart visualization
const maxVadHistory = 50 // Keep 50 samples (~1.6 seconds at 32ms intervals)
// Tauri VAD functions
async function loadVADModel() {
if (isVADModelLoaded.value || isLoadingVADModel.value)
return
isLoadingVADModel.value = true
vadModelError.value = ''
try {
await invoke('plugin:proj-airi-tauri-plugin-audio-vad|load_model_silero_vad')
isVADModelLoaded.value = true
}
catch (error) {
vadModelError.value = error as string
console.error('Failed to load VAD model:', error)
}
finally {
isLoadingVADModel.value = false
}
}
async function processAudioChunkWithVAD(audioData: Float32Array) {
if (!isVADModelLoaded.value)
return
try {
// Convert Float32Array to regular array for Tauri
const chunk = Array.from(audioData)
const probability = await invoke('plugin:proj-airi-tauri-plugin-audio-vad|audio_vad', { chunk })
if (probability != null) {
vadProbability.value = probability
// Update VAD history for visualization
vadHistory.value.push(probability)
if (vadHistory.value.length > maxVadHistory) {
vadHistory.value.shift()
}
}
// Update speaking detection based on VAD
if (useVADModel.value) {
isSpeaking.value = vadProbability.value > vadThreshold.value
}
}
catch (error) {
console.error('VAD processing error:', error)
// Fall back to volume-based detection
if (!useVADModel.value) {
isSpeaking.value = volumeLevel.value > speakingThreshold.value
}
}
}
function startVADProcessing() {
if (vadProcessingInterval.value)
return
vadProcessingInterval.value = window.setInterval(async () => {
if (audioChunkBuffer.value && audioChunkBuffer.value.length >= chunkSize) {
// Process the chunk with Tauri VAD
const chunk = audioChunkBuffer.value.slice(0, chunkSize)
await processAudioChunkWithVAD(chunk)
// Remove processed samples from buffer
const remaining = audioChunkBuffer.value.slice(chunkSize)
audioChunkBuffer.value = remaining.length > 0 ? remaining : new Float32Array(0)
}
}, 32) // 32ms intervals (chunkSize / sampleRate * 1000)
}
function stopVADProcessing() {
if (vadProcessingInterval.value) {
clearInterval(vadProcessingInterval.value)
vadProcessingInterval.value = null
}
audioChunkBuffer.value = null
vadProbability.value = 0
vadHistory.value = []
}
// Audio monitoring
async function setupAudioMonitoring() {
try {
if (!selectedAudioInput.value) {
console.warn('No audio input device selected')
return
}
// Clean up existing connections
await stopAudioMonitoring()
// Get user media with selected device
mediaStream.value = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: selectedAudioInput.value,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
})
// Create audio context
audioContext.value = new AudioContext()
const source = audioContext.value.createMediaStreamSource(mediaStream.value)
// Create analyser for volume detection
analyser.value = audioContext.value.createAnalyser()
analyser.value.fftSize = 256
analyser.value.smoothingTimeConstant = 0.3
// Create gain node for playback volume control
gainNode.value = audioContext.value.createGain()
gainNode.value.gain.value = enablePlayback.value ? (monitorVolume.value / 100) : 0
// Connect audio graph
source.connect(analyser.value)
if (enablePlayback.value) {
source.connect(gainNode.value)
gainNode.value.connect(audioContext.value.destination)
}
// Set up data array for analysis
const bufferLength = analyser.value.frequencyBinCount
dataArray.value = new Uint8Array(bufferLength)
// Start audio analysis loop
startAudioAnalysis()
// Load VAD model and start VAD processing if enabled
if (useVADModel.value) {
await loadVADModel()
startVADProcessing()
audioChunkBuffer.value = new Float32Array(0)
}
}
catch (error) {
console.error('Error setting up audio monitoring:', error)
}
}
async function stopAudioMonitoring() {
// Stop animation frame
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
animationFrame.value = undefined
}
// Stop media stream
if (mediaStream.value) {
mediaStream.value.getTracks().forEach(track => track.stop())
mediaStream.value = undefined
}
// Close audio context
if (audioContext.value) {
await audioContext.value.close()
audioContext.value = undefined
}
analyser.value = undefined
gainNode.value = undefined
dataArray.value = undefined
volumeLevel.value = 0
isSpeaking.value = false
// Stop VAD processing
stopVADProcessing()
}
function startAudioAnalysis() {
const analyze = () => {
if (!analyser.value || !dataArray.value)
return
// Get frequency data for volume visualization
analyser.value.getByteFrequencyData(dataArray.value)
// Calculate RMS volume level
let sum = 0
for (let i = 0; i < dataArray.value.length; i++) {
sum += dataArray.value[i] * dataArray.value[i]
}
const rms = Math.sqrt(sum / dataArray.value.length)
volumeLevel.value = Math.min(100, (rms / 255) * 100 * 3) // Amplify for better visualization
// Fallback speaking detection (when VAD model is not used)
if (!useVADModel.value || !isVADModelLoaded.value) {
isSpeaking.value = volumeLevel.value > speakingThreshold.value
}
// Collect audio samples for VAD processing
if (useVADModel.value && isVADModelLoaded.value && audioChunkBuffer.value !== null) {
// Get time domain data for VAD (raw audio samples)
const timeDataArray = new Float32Array(analyser.value.fftSize)
analyser.value.getFloatTimeDomainData(timeDataArray)
// Append new samples to buffer
const currentBuffer = audioChunkBuffer.value
const newBuffer = new Float32Array(currentBuffer.length + timeDataArray.length)
newBuffer.set(currentBuffer, 0)
newBuffer.set(timeDataArray, currentBuffer.length)
audioChunkBuffer.value = newBuffer
}
animationFrame.value = requestAnimationFrame(analyze)
}
analyze()
}
// Update playback routing when playback setting changes
async function updatePlayback() {
if (!audioContext.value || !gainNode.value)
return
if (enablePlayback.value) {
gainNode.value.gain.value = monitorVolume.value / 100
gainNode.value.connect(audioContext.value.destination)
}
else {
gainNode.value.gain.value = 0
gainNode.value.disconnect()
}
}
// Watchers
watch(selectedAudioInput, async () => {
if (isMonitoring.value) {
await setupAudioMonitoring()
}
})
watch(enablePlayback, updatePlayback)
watch(monitorVolume, () => {
if (gainNode.value && enablePlayback.value) {
gainNode.value.gain.value = monitorVolume.value / 100
}
})
watch(audioInputs, () => {
if (!selectedAudioInput.value && audioInputs.value.length > 0) {
selectedAudioInput.value = audioInputs.value[0]?.deviceId
}
})
watch(selectedAudioInput, async () => {
if (isMonitoring.value) {
await stopAudioMonitoring()
await setupAudioMonitoring()
}
})
// Monitoring toggle
async function toggleMonitoring() {
if (isMonitoring.value) {
await setupAudioMonitoring()
}
else {
await stopAudioMonitoring()
}
}
// Speaking indicator with enhanced VAD visualization
const speakingIndicatorClass = computed(() => {
if (!useVADModel.value || !isVADModelLoaded.value) {
// Volume-based: simple green/white
return isSpeaking.value
? 'bg-green-500 shadow-lg shadow-green-500/50'
: 'bg-white dark:bg-neutral-900 border-2 border-neutral-300 dark:border-neutral-600'
}
// VAD-based: color intensity based on probability
const prob = vadProbability.value
const threshold = vadThreshold.value
if (prob > threshold) {
// Speaking: green (could add intensity in future)
return `bg-green-500 shadow-lg shadow-green-500/50`
}
else if (prob > threshold * 0.5) {
// Close to threshold: yellow
return 'bg-yellow-500 shadow-lg shadow-yellow-500/30'
}
else {
// Low probability: neutral
return 'bg-white dark:bg-neutral-900 border-2 border-neutral-300 dark:border-neutral-600'
}
})
// Lifecycle
onMounted(() => {
devices.ensurePermissions().then(() => nextTick()).then(() => {
if (audioInputs.value.length > 0 && !selectedAudioInput.value) {
selectedAudioInput.value = audioInputs.value[0]?.deviceId
}
})
})
onUnmounted(() => {
stopAudioMonitoring()
})
</script>
<template>
<div>
Hearing
<div class="space-y-6">
<!-- Audio Input Selection -->
<div>
<FieldSelect
v-model="selectedAudioInput"
label="Audio Input Device"
description="Select the audio input device for your hearing module."
:options="audioInputs.map(input => ({
label: input.label || input.deviceId,
value: input.deviceId,
}))"
placeholder="Select an audio input device"
/>
</div>
<!-- Audio Monitoring Controls -->
<div class="rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800">
<h3 class="mb-4 flex items-center gap-2 text-lg">
<div class="text-xl" i-solar:volume-loud-bold-duotone />
Monitoring
</h3>
<div class="space-y-4">
<!-- Start/Stop Monitoring -->
<FieldCheckbox
v-model="isMonitoring"
label="Enable Audio Monitoring"
description="Start monitoring audio input levels and voice activity detection"
@update:model-value="toggleMonitoring"
/>
<!-- Audio Level Visualization -->
<div v-if="isMonitoring" class="space-y-3">
<!-- Volume Meter -->
<LevelMeter :level="volumeLevel" label="Input Level" />
<!-- VAD Probability Meter (when VAD model is active) -->
<ThresholdMeter
v-if="useVADModel && isVADModelLoaded"
:value="vadProbability"
:threshold="vadThreshold"
label="Probability of Speech"
below-label="Silence"
above-label="Speech"
threshold-label="Detection threshold"
/>
<!-- Threshold Controls -->
<div v-if="useVADModel && isVADModelLoaded" class="space-y-3">
<FieldRange
v-model="vadThreshold"
label="Sensitivity"
description="Adjust the threshold for speech detection"
:min="0.1"
:max="0.9"
:step="0.05"
:format-value="value => `${(value * 100).toFixed(0)}%`"
/>
</div>
<div v-else class="space-y-3">
<FieldRange
v-model="speakingThreshold"
label="Sensitivity"
description="Adjust the threshold for speech detection"
:min="1"
:max="80"
:step="1"
:format-value="value => `${value}%`"
/>
</div>
<!-- Speaking Indicator -->
<div class="flex items-center gap-3">
<div
class="h-4 w-4 rounded-full transition-all duration-200"
:class="speakingIndicatorClass"
/>
<span class="text-sm font-medium">
{{ isSpeaking ? 'Speaking Detected' : 'Silence' }}
</span>
<span class="ml-auto text-xs text-neutral-500">
{{ useVADModel && isVADModelLoaded ? 'Model Based' : 'Volume Based' }}
</span>
</div>
<!-- VAD Method Selection -->
<div class="border-t border-neutral-200 pt-3 dark:border-neutral-700">
<FieldCheckbox
v-model="useVADModel"
label="Model Based"
description="Use AI models for more accurate speech detection"
/>
<!-- VAD Model Status -->
<div v-if="useVADModel" class="mt-3 space-y-2">
<div v-if="isLoadingVADModel" class="flex items-center gap-2 text-primary-600 dark:text-primary-400">
<div class="animate-spin text-sm" i-solar:spinner-line-duotone />
<span class="text-sm">Loading...</span>
</div>
<div v-else-if="vadModelError" class="flex items-center gap-2 text-red-600 dark:text-red-400">
<div class="text-sm" i-solar:close-circle-bold-duotone />
<span class="text-sm">Inference error: {{ vadModelError }}</span>
</div>
<div v-else-if="isVADModelLoaded" class="flex items-center gap-2 text-green-600 dark:text-green-400">
<div class="text-sm" i-solar:check-circle-bold-duotone />
<span class="text-sm">Activated</span>
<span class="ml-auto text-xs text-neutral-500">
Probability: {{ (vadProbability * 100).toFixed(1) }}%
</span>
</div>
</div>
</div>
<!-- Voice Activity Visualization (when VAD model is active) -->
<TimeSeriesChart
v-if="useVADModel && isVADModelLoaded"
:history="vadHistory"
:current-value="vadProbability"
:threshold="vadThreshold"
:is-active="isSpeaking"
title="Voice Activity"
subtitle="Last 2 seconds"
active-label="Speaking"
active-legend-label="Voice detected"
inactive-legend-label="Silence"
threshold-label="Speech threshold"
/>
</div>
<!-- Audio Playback (Monitor) -->
<div v-if="isMonitoring" class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
<FieldCheckbox
v-model="enablePlayback"
label="Monitor Audio (Listen)"
description="Enable audio playback monitoring (like OBS). Be careful of feedback!"
/>
<div v-if="enablePlayback" class="mt-3">
<FieldRange
v-model="monitorVolume"
label="Monitor Volume"
description="Control the volume of audio monitoring playback"
:min="0"
:max="100"
:step="5"
:format-value="value => `${value}%`"
/>
</div>
</div>
<!-- Warning for playback -->
<div v-if="enablePlayback" class="border border-amber-200 rounded-lg bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-300">
<div class="text-sm" i-solar:warning-circle-bold-duotone />
<span class="text-sm font-medium">Audio feedback warning</span>
</div>
<div class="mt-1 text-xs text-amber-600 dark:text-amber-400">
Use headphones to prevent audio feedback. Lower the monitor volume if you hear echoing.
</div>
</div>
</div>
</div>
</div>
</template>
@@ -43,7 +43,7 @@ const modulesList = computed<Module[]>(() => [
name: t('settings.pages.modules.hearing.title'),
description: t('settings.pages.modules.hearing.description'),
icon: 'i-solar:microphone-3-bold-duotone',
to: '',
to: '/settings/modules/hearing',
configured: false,
},
{
@@ -72,10 +72,10 @@ const performanceThresholds = [
id="basic-examples"
title="Basic Examples"
>
<div class="p-2 space-y-6">
<div class="space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- Audio Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Audio Level (Real-time)
</h3>
@@ -87,7 +87,7 @@ const performanceThresholds = [
</div>
<!-- Battery Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Battery Level
</h3>
@@ -100,7 +100,7 @@ const performanceThresholds = [
</div>
<!-- CPU Usage -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
CPU Usage (Animated)
</h3>
@@ -119,10 +119,10 @@ const performanceThresholds = [
id="custom-styling"
title="Custom Styling"
>
<div class="p-2 space-y-6">
<div class="space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Temperature (Custom Colors) -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Temperature Monitor
</h3>
@@ -139,7 +139,7 @@ const performanceThresholds = [
</div>
<!-- Audio dB Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Audio Level (dB)
</h3>
@@ -162,10 +162,10 @@ const performanceThresholds = [
id="configuration-options"
title="Configuration Options"
>
<div class="p-2 space-y-6">
<div class="space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- Compact (No Header) -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Compact Mode
</h3>
@@ -180,7 +180,7 @@ const performanceThresholds = [
</div>
<!-- Many Bars -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
High Resolution
</h3>
@@ -196,7 +196,7 @@ const performanceThresholds = [
</div>
<!-- Few Bars -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Low Resolution
</h3>
@@ -218,8 +218,8 @@ const performanceThresholds = [
id="dashboard-example"
title="Dashboard Example"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="space-y-6">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-6 text-lg text-neutral-700 font-medium dark:text-neutral-300">
System Monitor Dashboard
</h3>
@@ -271,8 +271,8 @@ const performanceThresholds = [
id="interactive-demo"
title="Interactive Demo"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="space-y-6">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Adjustable Level
</h3>
@@ -41,7 +41,7 @@ const levelBars = computed(() => {
}))
})
function getBarColor(index: number, barLevel: number): string {
function getBarColor(_index: number, barLevel: number): string {
const thresholds = [...props.colorThresholds].sort((a, b) => a.value - b.value)
for (const threshold of thresholds) {
@@ -65,7 +65,7 @@ function getBarColor(index: number, barLevel: number): string {
<!-- Level Bars -->
<div
class="flex items-end gap-1 rounded bg-neutral-200 p-1 dark:bg-neutral-700"
class="flex items-end gap-1 rounded bg-neutral-200/45 p-1 dark:bg-neutral-700"
:style="{ height: `${height}px` }"
>
<div
@@ -73,7 +73,7 @@ function getBarColor(index: number, barLevel: number): string {
:key="index"
class="flex-1 rounded-sm transition-all"
:class="[
bar.active ? getBarColor(index, bar.level) : 'bg-neutral-300 dark:bg-neutral-600',
bar.active ? getBarColor(index, bar.level) : 'bg-neutral-200 dark:bg-neutral-600',
`duration-${animationSpeed}`,
]"
:style="{ height: bar.active ? '100%' : '20%' }"
@@ -57,7 +57,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- Detection Confidence -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Detection Confidence
</h3>
@@ -72,7 +72,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Quality Check -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Quality Threshold
</h3>
@@ -87,7 +87,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Real-time Monitoring -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Real-time Monitoring
</h3>
@@ -112,7 +112,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Model Confidence -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
ML Model Confidence
</h3>
@@ -130,7 +130,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Anomaly Detection -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Anomaly Detection
</h3>
@@ -157,7 +157,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<!-- Response Time -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Response Time
</h3>
@@ -175,7 +175,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Success Rate -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Success Rate
</h3>
@@ -191,7 +191,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Load Factor -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Load Factor
</h3>
@@ -217,7 +217,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- No Legend -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Minimal Display
</h3>
@@ -234,7 +234,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- High Resolution -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
High Precision
</h3>
@@ -252,7 +252,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
</div>
<!-- Custom Range -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Custom Range
</h3>
@@ -278,7 +278,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
title="Interactive Demo"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Adjustable Parameters
</h3>
@@ -333,7 +333,7 @@ const formatScore = (value: number) => `${value.toFixed(3)}`
title="Dashboard Example"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<div class="rounded-xl bg-white px-3 py-2 shadow-md dark:bg-neutral-800">
<h3 class="mb-6 text-lg text-neutral-700 font-medium dark:text-neutral-300">
System Health Dashboard
</h3>