feat(drizzle-duckdb-wasm): memory decay demo
This commit is contained in:
@@ -39,6 +39,13 @@ const toggleDark = useToggle(isDark)
|
||||
>
|
||||
<h1>Graph</h1>
|
||||
</RouterLink>
|
||||
<div bg="neutral-200 dark:neutral-600" h="1lh" w="0.5" />
|
||||
<RouterLink
|
||||
to="/memory-decay" px-3 py-2 bg="hover:neutral-200 dark:hover:neutral-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
>
|
||||
<h1>Memory Decay</h1>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-model="modelValue"
|
||||
transition="background duration-250 ease-in-out"
|
||||
outline="focus-within:none"
|
||||
flex="~"
|
||||
border="neutral-300 dark:neutral-700 data-[state=checked]:primary-200 data-[state=unchecked]:neutral-300 focus-within:neutral-800"
|
||||
bg="data-[state=checked]:primary-400 data-[state=unchecked]:neutral-300 data-[state=checked]:dark:primary-400/80 dark:data-[state=unchecked]:neutral-800"
|
||||
relative h-7 w="12.5" rounded-full
|
||||
shadow="sm focus-within:shadow-neutral-800 focus-within:[0_0_0_1px] "
|
||||
>
|
||||
<SwitchThumb
|
||||
my-auto size-6
|
||||
flex items-center justify-center
|
||||
translate-x="0.5 data-[state=checked]:full"
|
||||
rounded-full bg-white text-xs shadow-xl
|
||||
transition="transform duration-250 ease-in-out"
|
||||
will-change-transform
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dark [border~='dark\:neutral-700'] {
|
||||
--un-border-opacity: 1;
|
||||
border-color: rgb(64 64 64 / var(--un-border-opacity));
|
||||
}
|
||||
|
||||
[border~='data-\[state\=checked\]\:primary-200'][data-state='checked'] {
|
||||
--un-border-opacity: 1;
|
||||
border-color: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0) / var(--un-border-opacity));
|
||||
}
|
||||
|
||||
[bg~='data-\[state\=checked\]\:primary-400'][data-state='checked'] {
|
||||
--un-bg-opacity: 1;
|
||||
background-color: oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + 0) / var(--un-bg-opacity));
|
||||
}
|
||||
|
||||
.dark [bg~='data-\[state\=checked\]\:dark\:primary-400\/80'][data-state='checked'] {
|
||||
background-color: oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + 0) / 0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Checkbox from '../Checkbox.vue'
|
||||
import Range from '../Range.vue'
|
||||
|
||||
const longTermMemoryEnabled = defineModel<boolean>('longTermMemoryEnabled', { default: false })
|
||||
const longTermMemoryThreshold = defineModel<number>('longTermMemoryThreshold', { default: 1 })
|
||||
const longTermMemoryStability = defineModel<number>('longTermMemoryStability', { default: 0.5 })
|
||||
const longTermMemoryVisualize = defineModel<boolean>('longTermMemoryVisualize', { default: false })
|
||||
const retrievalBoost = defineModel<number>('retrievalBoost', { default: 0.5 })
|
||||
const retrievalDecaySlowdown = defineModel<number>('retrievalDecaySlowdown', { default: 0.5 })
|
||||
const decayRate = defineModel<number>('decayRate', { default: 0.01 })
|
||||
const timeUnit = defineModel<string>('timeUnit', { default: 'days' })
|
||||
const maxDaysToShow = defineModel<number>('maxDaysToShow', { default: 30 })
|
||||
|
||||
// Computed percent values for display
|
||||
const retrievalBoostPercent = computed(() => (retrievalBoost.value * 100).toFixed(0))
|
||||
const decaySlowdownPercent = computed(() => (retrievalDecaySlowdown.value * 100).toFixed(0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Long-term Memory Model -->
|
||||
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Long-Term Memory Model
|
||||
</h2>
|
||||
<Checkbox v-model="longTermMemoryEnabled" />
|
||||
</div>
|
||||
|
||||
<p class="mb-4 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Configure how memories transition from short-term to permanent long-term memory with repeated retrievals
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Retrieval Threshold</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="longTermMemoryThreshold"
|
||||
:min="1"
|
||||
:max="20"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
:disabled="!longTermMemoryEnabled"
|
||||
/>
|
||||
<span class="w-10 text-right font-mono">{{ longTermMemoryEnabled }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
Retrievals needed to form stable long-term memory
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Stability Factor</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="longTermMemoryStability"
|
||||
:min="0.01"
|
||||
:max="0.9"
|
||||
:step="0.01"
|
||||
class="w-full"
|
||||
:disabled="!longTermMemoryEnabled"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ longTermMemoryStability.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How quickly memories stabilize (lower = faster stabilization)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Memory Model Settings</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="longTermMemoryVisualize" />
|
||||
<label for="ltmVisualize" class="text-sm">Show LTM projection</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
Options for memory visualization
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retrieval Effect Settings -->
|
||||
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
|
||||
<h2 class="mb-2 text-lg font-semibold">
|
||||
Retrieval Effect Settings
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Retrieval Boost Factor</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="retrievalBoost"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ retrievalBoostPercent }}%</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How much each retrieval strengthens memory
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Decay Slowdown Factor</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="retrievalDecaySlowdown"
|
||||
:min="0.1"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ decaySlowdownPercent }}%</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How much retrievals slow future decay (lower = more slowdown)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Decay Controls -->
|
||||
<div class="grid grid-cols-1 mb-4 gap-4 rounded-lg bg-neutral-100 p-4 md:grid-cols-3 dark:bg-neutral-800/50">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Base Decay Rate</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="decayRate"
|
||||
:min="0.01"
|
||||
:max="0.5"
|
||||
:step="0.01"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ decayRate.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
Controls decay speed (0.0990 = ~10% per day)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Time Unit</label>
|
||||
<select
|
||||
v-model="timeUnit"
|
||||
class="w-full rounded-lg border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-700/30"
|
||||
>
|
||||
<option value="hours">
|
||||
Hours
|
||||
</option>
|
||||
<option value="days">
|
||||
Days
|
||||
</option>
|
||||
<option value="weeks">
|
||||
Weeks
|
||||
</option>
|
||||
<option value="months">
|
||||
Months
|
||||
</option>
|
||||
</select>
|
||||
<div class="text-xs text-neutral-500">
|
||||
Unit used in calculations (affects decay rate)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Future Projection (Days)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="maxDaysToShow"
|
||||
:min="7"
|
||||
:max="90"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ maxDaysToShow }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How many days to project into the future
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,224 @@
|
||||
<!-- src/components/TimeControls.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits([
|
||||
'timeJump',
|
||||
])
|
||||
|
||||
const simulatedTimeOffset = defineModel<number>('simulatedTimeOffset', { default: 0 })
|
||||
const isTimeAccelerated = defineModel<boolean>('isTimeAccelerated', { default: false })
|
||||
const timeMultiplier = defineModel<number>('timeMultiplier', { default: 60 * 60 * 24 })
|
||||
|
||||
const customTimeValue = ref(1)
|
||||
const customTimeUnit = ref('days')
|
||||
|
||||
const timeMultiplierPresets = [
|
||||
{ label: '1 hour/s', value: 60 * 60 },
|
||||
{ label: '6 hours/s', value: 60 * 60 * 6 },
|
||||
{ label: '1 day/s', value: 60 * 60 * 24 },
|
||||
{ label: '3 days/s', value: 60 * 60 * 24 * 3 },
|
||||
{ label: '1 week/s', value: 60 * 60 * 24 * 7 },
|
||||
{ label: '1 month/s', value: 60 * 60 * 24 * 30 },
|
||||
]
|
||||
|
||||
// Format the current time with offset
|
||||
const currentSimulatedTime = computed(() => {
|
||||
const now = new Date()
|
||||
now.setSeconds(now.getSeconds() + simulatedTimeOffset.value)
|
||||
return now.toLocaleString()
|
||||
})
|
||||
|
||||
// Format time multiplier for display
|
||||
const formattedTimeMultiplier = computed(() => {
|
||||
const multiplier = timeMultiplier.value
|
||||
|
||||
if (multiplier === 60 * 60)
|
||||
return '1 hour/s'
|
||||
if (multiplier === 60 * 60 * 6)
|
||||
return '6 hours/s'
|
||||
if (multiplier === 60 * 60 * 24)
|
||||
return '1 day/s'
|
||||
if (multiplier === 60 * 60 * 24 * 3)
|
||||
return '3 days/s'
|
||||
if (multiplier === 60 * 60 * 24 * 7)
|
||||
return '1 week/s'
|
||||
if (multiplier === 60 * 60 * 24 * 30)
|
||||
return '1 month/s'
|
||||
|
||||
if (multiplier < 60)
|
||||
return `${multiplier} seconds/s`
|
||||
if (multiplier < 60 * 60)
|
||||
return `${Math.round(multiplier / 60)} minutes/s`
|
||||
if (multiplier < 60 * 60 * 24)
|
||||
return `${Math.round(multiplier / (60 * 60))} hours/s`
|
||||
if (multiplier < 60 * 60 * 24 * 7)
|
||||
return `${Math.round(multiplier / (60 * 60 * 24))} days/s`
|
||||
if (multiplier < 60 * 60 * 24 * 30)
|
||||
return `${Math.round(multiplier / (60 * 60 * 24 * 7))} weeks/s`
|
||||
return `${Math.round(multiplier / (60 * 60 * 24 * 30))} months/s`
|
||||
})
|
||||
|
||||
function toggleTimeAcceleration() {
|
||||
isTimeAccelerated.value = !isTimeAccelerated.value
|
||||
}
|
||||
|
||||
function resetTime() {
|
||||
simulatedTimeOffset.value = 0
|
||||
isTimeAccelerated.value = false
|
||||
}
|
||||
|
||||
function setCustomTimeMultiplier() {
|
||||
let multiplier = customTimeValue.value
|
||||
|
||||
switch (customTimeUnit.value) {
|
||||
case 'seconds':
|
||||
break
|
||||
case 'minutes':
|
||||
multiplier *= 60
|
||||
break
|
||||
case 'hours':
|
||||
multiplier *= 60 * 60
|
||||
break
|
||||
case 'days':
|
||||
multiplier *= 60 * 60 * 24
|
||||
break
|
||||
case 'weeks':
|
||||
multiplier *= 60 * 60 * 24 * 7
|
||||
break
|
||||
case 'months':
|
||||
multiplier *= 60 * 60 * 24 * 30
|
||||
break
|
||||
}
|
||||
|
||||
timeMultiplier.value = multiplier
|
||||
}
|
||||
|
||||
function jumpAhead(amount, unit) {
|
||||
emit('timeJump', { amount, unit })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
|
||||
<h2 class="mb-2 text-lg font-semibold">
|
||||
Time Simulation
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="text-sm font-mono" flex-1>
|
||||
{{ currentSimulatedTime }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
:class="{ 'bg-red-100 dark:bg-red-900': isTimeAccelerated, 'bg-green-100 dark:bg-green-900': !isTimeAccelerated }"
|
||||
class="rounded-lg px-4 py-2 font-medium transition-colors"
|
||||
@click="toggleTimeAcceleration"
|
||||
>
|
||||
<div v-if="isTimeAccelerated" i-solar:play-bold />
|
||||
<div v-else i-solar:pause-bold />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-neutral-200 px-4 py-2 font-medium dark:bg-neutral-700"
|
||||
@click="resetTime"
|
||||
>
|
||||
<div i-solar:restart-line-duotone />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Time jump shortcuts -->
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="jumpAhead(1, 'hour')"
|
||||
>
|
||||
+1 Hour
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="jumpAhead(1, 'day')"
|
||||
>
|
||||
+1 Day
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="jumpAhead(1, 'week')"
|
||||
>
|
||||
+1 Week
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="jumpAhead(1, 'month')"
|
||||
>
|
||||
+1 Month
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 text-lg font-semibold">
|
||||
Speed
|
||||
</h2>
|
||||
<div class="mt-4 font-medium">
|
||||
{{ formattedTimeMultiplier }}
|
||||
</div>
|
||||
|
||||
<!-- Time multiplier presets -->
|
||||
<div class="grid grid-cols-2 mt-4 gap-2 md:grid-cols-6 sm:grid-cols-3">
|
||||
<button
|
||||
v-for="preset in timeMultiplierPresets"
|
||||
:key="preset.value"
|
||||
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
||||
:class="timeMultiplier === preset.value ? 'bg-blue-200 dark:bg-blue-800' : 'bg-blue-100 dark:bg-blue-900'"
|
||||
@click="timeMultiplier = preset.value"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom time multiplier -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||
<div class="text-sm font-medium">
|
||||
Custom:
|
||||
</div>
|
||||
<input
|
||||
v-model.number="customTimeValue"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-20 rounded-lg border-none bg-white p-2 dark:bg-neutral-700"
|
||||
>
|
||||
<select
|
||||
v-model="customTimeUnit"
|
||||
class="rounded-lg border-none bg-white p-2 dark:bg-neutral-700"
|
||||
>
|
||||
<option value="seconds">
|
||||
seconds/s
|
||||
</option>
|
||||
<option value="minutes">
|
||||
minutes/s
|
||||
</option>
|
||||
<option value="hours">
|
||||
hours/s
|
||||
</option>
|
||||
<option value="days">
|
||||
days/s
|
||||
</option>
|
||||
<option value="weeks">
|
||||
weeks/s
|
||||
</option>
|
||||
<option value="months">
|
||||
months/s
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-2 text-sm font-medium dark:bg-blue-900"
|
||||
@click="setCustomTimeMultiplier"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,192 @@
|
||||
<!-- src/components/MemoryDetails.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
memory: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
longTermMemoryEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
longTermMemoryThreshold: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['retrieve'])
|
||||
|
||||
// Calculate age in days
|
||||
const ageInDays = computed(() => {
|
||||
return Math.round(props.memory.age_in_seconds / (24 * 60 * 60))
|
||||
})
|
||||
|
||||
// Calculate memory status
|
||||
const memoryStatus = computed(() => {
|
||||
if (props.longTermMemoryEnabled && props.memory.retrieval_count >= props.longTermMemoryThreshold) {
|
||||
return {
|
||||
type: 'long-term',
|
||||
label: `Long-term`,
|
||||
color: 'text-purple-600 dark:text-purple-400',
|
||||
}
|
||||
}
|
||||
else if (props.memory.retrieval_count > 0) {
|
||||
return {
|
||||
type: 'working',
|
||||
label: props.longTermMemoryEnabled
|
||||
? `Working`
|
||||
: 'Working memory',
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
type: 'short-term',
|
||||
label: 'Short-term',
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Progress percentage for memory strength
|
||||
const strengthPercentage = computed(() => {
|
||||
return Math.round((props.memory.decayed_score / props.memory.score) * 100)
|
||||
})
|
||||
|
||||
// Progress percentage for LTM
|
||||
const ltmPercentage = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
|
||||
return Math.round(Number.parseFloat(props.memory.ltm_factor || 0) * 100)
|
||||
}
|
||||
else {
|
||||
return Math.round((props.memory.retrieval_count / props.longTermMemoryThreshold) * 100)
|
||||
}
|
||||
})
|
||||
|
||||
function simulateRetrieval() {
|
||||
emit('retrieve', props.memory.storyid)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="font-bold">
|
||||
{{ memory.storyid }}
|
||||
</h3>
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-xs font-medium dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800" h-fit
|
||||
@click="simulateRetrieval"
|
||||
>
|
||||
Simulate Retrieval
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="grid grid-cols-2 mb-6 mt-2 gap-3" text-sm font-mono>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Phase:
|
||||
</div>
|
||||
<div class="font-medium" text-right text-nowrap :class="memoryStatus.color">
|
||||
{{ memoryStatus.label }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Original Score:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ Math.round(memory.score) }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Current Score:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ Math.round(memory.decayed_score) }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
% Remaining:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ strengthPercentage }}%
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Creation Date:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ new Date(memory.lastupdate).toLocaleDateString() }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Last Retrieved:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ new Date(memory.last_retrieved_at).toLocaleDateString() }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Retrieval Count:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ memory.retrieval_count }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Age:
|
||||
</div>
|
||||
<div class="font-medium" text-right>
|
||||
{{ ageInDays }} days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Visual representation of decay -->
|
||||
<div class="mt-4" font-mono>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Memory Strength:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full transition-all duration-500"
|
||||
:class="longTermMemoryEnabled && memory.retrieval_count >= longTermMemoryThreshold ? 'bg-purple-400' : 'bg-blue-500'"
|
||||
:style="`width: ${strengthPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-xs">
|
||||
<span>0%</span>
|
||||
<span>50%</span>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LTM Progress -->
|
||||
<div v-if="longTermMemoryEnabled && memory.retrieval_count > 0" class="mt-4" font-mono>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Long-term Memory Progress:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full bg-purple-400 transition-all duration-500"
|
||||
:style="`width: ${ltmPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between">
|
||||
<span class="text-xs">Short-term</span>
|
||||
<span v-if="memory.retrieval_count < longTermMemoryThreshold" class="text-sm font-medium">
|
||||
{{ memory.retrieval_count }}/{{ longTermMemoryThreshold }} retrievals
|
||||
</span>
|
||||
<span v-else class="text-sm text-purple-600 font-medium dark:text-purple-400">
|
||||
{{ Math.round(Number.parseFloat(memory.ltm_factor || 0) * 100) }}% stable
|
||||
</span>
|
||||
<span class="text-xs">Permanent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,581 @@
|
||||
<script setup lang="ts">
|
||||
import { useDark } from '@vueuse/core'
|
||||
import * as d3 from 'd3'
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memoryData: MemoryDataItem[]
|
||||
selectedStoryId: string
|
||||
decayRate: number
|
||||
maxDaysToShow: number
|
||||
longTermMemoryEnabled: boolean
|
||||
longTermMemoryThreshold: number
|
||||
longTermMemoryVisualize: boolean
|
||||
retrievalBoost: number
|
||||
retrievalDecaySlowdown: number
|
||||
}>()
|
||||
|
||||
interface MemoryDataItem {
|
||||
storyid: string
|
||||
score: number
|
||||
lastupdate: string // ISO timestamp
|
||||
last_retrieved_at: string // ISO timestamp
|
||||
retrieval_count: number
|
||||
lastupdate_str: string
|
||||
last_retrieved_str: string
|
||||
age_in_seconds: number
|
||||
time_since_retrieval: number
|
||||
ltm_factor?: number // Optional since it's only present when longTermMemoryEnabled is true
|
||||
decayed_score: number
|
||||
}
|
||||
|
||||
interface DataPoint {
|
||||
x: number
|
||||
y: number
|
||||
label: string
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
labels: string[]
|
||||
dataPoints: {
|
||||
withRetrievals: DataPoint[]
|
||||
withoutRetrievals: DataPoint[]
|
||||
ltmProjection: DataPoint[]
|
||||
}
|
||||
}
|
||||
|
||||
const chartContainer = ref(null)
|
||||
const tooltip = ref(null)
|
||||
let resizeObserver = null
|
||||
|
||||
const isDark = useDark()
|
||||
|
||||
// Render chart when dependencies change
|
||||
watchEffect(() => {
|
||||
if (chartContainer.value && props.memoryData.length && props.selectedStoryId) {
|
||||
renderChart()
|
||||
}
|
||||
})
|
||||
|
||||
// Render D3 chart
|
||||
function renderChart() {
|
||||
// Clear existing chart
|
||||
d3.select(chartContainer.value).selectAll('*').remove()
|
||||
|
||||
// Find the selected story
|
||||
const story = props.memoryData.find(s => s.storyid === props.selectedStoryId)
|
||||
if (!story)
|
||||
return
|
||||
|
||||
// Get container dimensions
|
||||
const containerRect = chartContainer.value.getBoundingClientRect()
|
||||
const containerWidth = containerRect.width
|
||||
const containerHeight = containerRect.height || 320
|
||||
|
||||
// Set chart margins
|
||||
const margin = { top: 20, right: 0, bottom: 20, left: 30 }
|
||||
const width = containerWidth - margin.left - margin.right
|
||||
const height = containerHeight - margin.top - margin.bottom
|
||||
|
||||
// Prepare data for chart
|
||||
const originalScore = Number.parseFloat(String(story.score))
|
||||
const dr = props.decayRate
|
||||
const maxDays = props.maxDaysToShow
|
||||
const ageInDays = Number.parseFloat(String(story.age_in_seconds)) / (24 * 60 * 60)
|
||||
const retrievals = Number.parseInt(String(story.retrieval_count))
|
||||
const daysSinceRetrieval = Number.parseFloat(String(story.time_since_retrieval)) / (24 * 60 * 60)
|
||||
const ltmFactor = props.longTermMemoryEnabled ? Number.parseFloat(String(story.ltm_factor || '0')) : 0
|
||||
|
||||
// Generate data points
|
||||
const chartData = {
|
||||
labels: ['Original', 'Current'],
|
||||
dataPoints: {
|
||||
withRetrievals: [
|
||||
{ x: 0, y: originalScore, label: 'Original' },
|
||||
{ x: ageInDays, y: Number.parseFloat(String(story.decayed_score)), label: 'Current' },
|
||||
],
|
||||
withoutRetrievals: [
|
||||
{ x: 0, y: originalScore, label: 'Original' },
|
||||
{ x: ageInDays, y: originalScore * Math.exp(-dr * ageInDays * (1 - ltmFactor)), label: 'Current' },
|
||||
],
|
||||
ltmProjection: props.longTermMemoryVisualize
|
||||
? [
|
||||
{ x: 0, y: originalScore, label: 'Original' },
|
||||
{
|
||||
x: ageInDays,
|
||||
y: retrievals >= props.longTermMemoryThreshold
|
||||
? originalScore * (1 - Math.max(0, 0.1 - 0.1 * retrievals))
|
||||
: Number.parseFloat(String(story.decayed_score)),
|
||||
label: 'Current',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
}
|
||||
|
||||
// Add future projection points
|
||||
const dayStep = Math.ceil(maxDays / 10)
|
||||
for (let day = dayStep; day <= maxDays; day += dayStep) {
|
||||
const futureDay = ageInDays + day
|
||||
const futureDaysSinceRetrieval = daysSinceRetrieval + day
|
||||
const label = `+${day}d`
|
||||
|
||||
// Point with retrieval effects
|
||||
const scoreWithRetrievals = originalScore
|
||||
* Math.exp(-dr * futureDay * (1 - ltmFactor))
|
||||
* (1 + (retrievals * props.retrievalBoost
|
||||
* Math.exp(-dr * props.retrievalDecaySlowdown * futureDaysSinceRetrieval)))
|
||||
|
||||
// Point without retrieval effects
|
||||
const scoreWithoutRetrievals = originalScore * Math.exp(-dr * futureDay * (1 - ltmFactor))
|
||||
|
||||
// Add to data series
|
||||
chartData.labels.push(label)
|
||||
chartData.dataPoints.withRetrievals.push({ x: futureDay, y: scoreWithRetrievals, label })
|
||||
chartData.dataPoints.withoutRetrievals.push({ x: futureDay, y: scoreWithoutRetrievals, label })
|
||||
|
||||
// Add LTM projection if enabled
|
||||
if (props.longTermMemoryVisualize) {
|
||||
const ltmProjectionValue = retrievals >= props.longTermMemoryThreshold
|
||||
? originalScore * (1 - Math.max(0, 0.1 - 0.1 * retrievals)) * Math.exp(-0.01 * day)
|
||||
: scoreWithRetrievals
|
||||
|
||||
chartData.dataPoints.ltmProjection.push({ x: futureDay, y: ltmProjectionValue, label })
|
||||
}
|
||||
}
|
||||
|
||||
// Create tooltip if it doesn't exist
|
||||
if (!tooltip.value) {
|
||||
tooltip.value = d3.select(chartContainer.value)
|
||||
.append('div')
|
||||
.attr('class', 'absolute pointer-events-none transition-opacity duration-200 bg-white dark:bg-neutral-800 p-2 rounded shadow-md text-sm z-10')
|
||||
.style('opacity', 0)
|
||||
}
|
||||
|
||||
// Create SVG
|
||||
const svg = d3.select(chartContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet')
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
|
||||
// Set up scales
|
||||
const xExtent = [0, maxDays + ageInDays]
|
||||
const yExtent = [0, d3.max([
|
||||
...chartData.dataPoints.withRetrievals,
|
||||
...chartData.dataPoints.withoutRetrievals,
|
||||
...chartData.dataPoints.ltmProjection,
|
||||
], d => d.y) * 1.1]
|
||||
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain(xExtent)
|
||||
.range([0, width])
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([height, 0])
|
||||
|
||||
// Add axes and gridlines
|
||||
addAxesAndGrids(svg, xScale, yScale, width, height)
|
||||
|
||||
// Add data lines and areas
|
||||
addDataLines(svg, chartData, xScale, yScale, height)
|
||||
|
||||
// Add half-life indicator
|
||||
const halfLife = Math.log(2) / dr
|
||||
if (halfLife <= maxDays + ageInDays) {
|
||||
addHalfLifeIndicator(svg, halfLife, xScale, height)
|
||||
}
|
||||
|
||||
// Add data points with interaction
|
||||
addDataPoints(svg, chartData.dataPoints.withRetrievals, xScale, yScale)
|
||||
|
||||
// Add chart title and legends
|
||||
addTitleAndLegends(svg, story.storyid, retrievals, ltmFactor, width)
|
||||
|
||||
// Add LTM indicators
|
||||
if (props.longTermMemoryEnabled) {
|
||||
addLTMIndicators(svg, retrievals, props.longTermMemoryThreshold, ltmFactor, width)
|
||||
}
|
||||
}
|
||||
|
||||
function addAxesAndGrids(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
xScale: d3.ScaleLinear<number, number>,
|
||||
yScale: d3.ScaleLinear<number, number>,
|
||||
width: number,
|
||||
height: number,
|
||||
): void {
|
||||
const xAxis = d3.axisBottom(xScale)
|
||||
.ticks(5)
|
||||
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())} days`)
|
||||
|
||||
const yAxis = d3.axisLeft(yScale)
|
||||
.ticks(5)
|
||||
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())}`)
|
||||
|
||||
// Add gridlines
|
||||
if (!isDark.value) {
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(
|
||||
d3.axisBottom(xScale)
|
||||
.tickSize(-height)
|
||||
.tickFormat(() => ''), // Use a function that returns empty string
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', '#e5e7eb')
|
||||
.attr('stroke-opacity', 0.5)
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.call(
|
||||
d3.axisLeft(yScale)
|
||||
.tickSize(-width)
|
||||
.tickFormat(() => ''), // Use a function that returns empty string
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', '#e5e7eb')
|
||||
.attr('stroke-opacity', 0.5)
|
||||
}
|
||||
else {
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(
|
||||
d3.axisBottom(xScale)
|
||||
.tickSize(-height)
|
||||
.tickFormat(() => ''), // Use a function that returns empty string
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', '#434345')
|
||||
.attr('stroke-opacity', 0.5)
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.call(
|
||||
d3.axisLeft(yScale)
|
||||
.tickSize(-width)
|
||||
.tickFormat(() => ''), // Use a function that returns empty string
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', '#434345')
|
||||
.attr('stroke-opacity', 0.5)
|
||||
}
|
||||
|
||||
// Add axes
|
||||
svg.append('g')
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(xAxis)
|
||||
.append('text')
|
||||
.attr('class', 'axis-label')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text('Time (days)')
|
||||
.attr('fill', 'currentColor')
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis)
|
||||
.append('text')
|
||||
.attr('class', 'axis-label')
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('y', -10)
|
||||
.attr('x', -height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text('Memory Strength')
|
||||
.attr('fill', 'currentColor')
|
||||
}
|
||||
|
||||
function addDataLines(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
chartData: ChartData,
|
||||
xScale: d3.ScaleLinear<number, number>,
|
||||
yScale: d3.ScaleLinear<number, number>,
|
||||
height: number,
|
||||
): void {
|
||||
// Create line generators with correct types
|
||||
const line = d3.line<DataPoint>()
|
||||
.x(d => xScale(d.x))
|
||||
.y(d => yScale(d.y))
|
||||
.curve(d3.curveMonotoneX)
|
||||
|
||||
// Create area generators with correct types
|
||||
const area = d3.area<DataPoint>()
|
||||
.x(d => xScale(d.x))
|
||||
.y0(height)
|
||||
.y1(d => yScale(d.y))
|
||||
.curve(d3.curveMonotoneX)
|
||||
|
||||
// Add area fill for without retrievals
|
||||
svg.append('path')
|
||||
.datum(chartData.dataPoints.withoutRetrievals)
|
||||
.attr('class', 'area without-retrievals')
|
||||
.attr('d', area)
|
||||
.attr('fill', 'rgba(156, 163, 175, 0.2)')
|
||||
|
||||
// Add line for without retrievals
|
||||
svg.append('path')
|
||||
.datum(chartData.dataPoints.withoutRetrievals)
|
||||
.attr('class', 'line without-retrievals')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(156, 163, 175, 0.6)')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,3')
|
||||
|
||||
// Add area fill for with retrievals
|
||||
svg.append('path')
|
||||
.datum(chartData.dataPoints.withRetrievals)
|
||||
.attr('class', 'area with-retrievals')
|
||||
.attr('d', area)
|
||||
.attr('fill', 'rgba(59, 130, 246, 0.2)')
|
||||
|
||||
// Add line for with retrievals
|
||||
svg.append('path')
|
||||
.datum(chartData.dataPoints.withRetrievals)
|
||||
.attr('class', 'line with-retrievals')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(59, 130, 246, 1)')
|
||||
.attr('stroke-width', 3)
|
||||
|
||||
// Add LTM projection line if enabled
|
||||
if (props.longTermMemoryVisualize && chartData.dataPoints.ltmProjection.length > 0) {
|
||||
svg.append('path')
|
||||
.datum(chartData.dataPoints.ltmProjection)
|
||||
.attr('class', 'line ltm-projection')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(139, 92, 246, 0.8)')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '3,2')
|
||||
}
|
||||
}
|
||||
|
||||
function addDataPoints(svg, dataPoints, xScale, yScale) {
|
||||
svg.selectAll('.point-with-retrievals')
|
||||
.data(dataPoints)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'point-with-retrievals')
|
||||
.attr('cx', d => xScale(d.x))
|
||||
.attr('cy', d => yScale(d.y))
|
||||
.attr('r', (d, i) => i < 2 ? 6 : 5)
|
||||
.attr('fill', (d, i) => i < 2 ? 'rgba(220, 38, 38, 0.8)' : 'rgba(59, 130, 246, 0.8)')
|
||||
.attr('stroke', 'white')
|
||||
.attr('stroke-width', 2)
|
||||
.on('mouseover', function (event, d) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', 8)
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 1)
|
||||
.html(`
|
||||
<div class="font-semibold">${d.label}</div>
|
||||
<div>Score: ${Math.round(d.y)}</div>
|
||||
<div>Day: ${Math.round(d.x)}</div>
|
||||
`)
|
||||
.style('left', `${event.offsetX + 15}px`)
|
||||
.style('top', `${event.offsetY - 28}px`)
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('r', (d, i) => i < 2 ? 6 : 5)
|
||||
|
||||
tooltip.value
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 0)
|
||||
})
|
||||
|
||||
// Add labels to important points
|
||||
svg.selectAll('.point-label')
|
||||
.data(dataPoints.filter((d, i) => i === 0 || i === 1 || i === dataPoints.length - 1 || i % 3 === 0))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'point-label')
|
||||
.attr('x', d => xScale(d.x))
|
||||
.attr('y', d => yScale(d.y) - 15)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(d => Math.round(d.y))
|
||||
}
|
||||
|
||||
function addHalfLifeIndicator(svg, halfLife, xScale, height) {
|
||||
svg.append('line')
|
||||
.attr('class', 'half-life-line')
|
||||
.attr('x1', xScale(halfLife))
|
||||
.attr('y1', 0)
|
||||
.attr('x2', xScale(halfLife))
|
||||
.attr('y2', height)
|
||||
.attr('stroke', 'rgba(220, 38, 38, 0.5)')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke-dasharray', '5,5')
|
||||
|
||||
svg.append('text')
|
||||
.attr('class', 'half-life-label')
|
||||
.attr('x', xScale(halfLife))
|
||||
.attr('y', 0)
|
||||
.attr('dy', -8)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', 'rgba(220, 38, 38, 0.8)')
|
||||
.text(`Half-life: ${halfLife.toFixed(1)} days`)
|
||||
}
|
||||
|
||||
function addTitleAndLegends(svg, storyId, retrievals, ltmFactor, width) {
|
||||
// Add chart title
|
||||
let titleText = `Memory Decay for ${storyId} (${retrievals} retrievals)`
|
||||
if (props.longTermMemoryEnabled) {
|
||||
if (retrievals >= props.longTermMemoryThreshold) {
|
||||
const ltmPercent = Math.round(ltmFactor * 100)
|
||||
titleText += ` - LTM: ${ltmPercent}%`
|
||||
}
|
||||
else if (retrievals > 0) {
|
||||
titleText += ` - LTM: ${retrievals}/${props.longTermMemoryThreshold} retrievals`
|
||||
}
|
||||
}
|
||||
|
||||
svg.append('text')
|
||||
.attr('class', 'chart-title')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '16px')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(titleText)
|
||||
|
||||
// Add legend
|
||||
const legendData = [
|
||||
{ label: `Current (${retrievals} retrievals)`, color: 'rgba(59, 130, 246, 0.8)' },
|
||||
{ label: 'Without retrievals', color: 'rgba(156, 163, 175, 0.8)' },
|
||||
]
|
||||
|
||||
if (props.longTermMemoryVisualize) {
|
||||
legendData.push({ label: 'Long-term memory', color: 'rgba(139, 92, 246, 0.8)' })
|
||||
}
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', `translate(${width - 180}, 10)`)
|
||||
|
||||
const legendItems = legend.selectAll('.legend-item')
|
||||
.data(legendData)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'legend-item')
|
||||
.attr('transform', (d, i) => `translate(10, ${i * 25 + 20})`)
|
||||
|
||||
legendItems.append('circle')
|
||||
.attr('r', 6)
|
||||
.attr('fill', d => d.color)
|
||||
.attr('stroke', 'white')
|
||||
.attr('stroke-width', 1)
|
||||
|
||||
legendItems.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 4)
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(d => d.label)
|
||||
}
|
||||
|
||||
function addLTMIndicators(svg, retrievals, threshold, ltmFactor, width) {
|
||||
if (retrievals > 0 && retrievals < threshold) {
|
||||
const progress = Math.round((retrievals / threshold) * 100)
|
||||
svg.append('g')
|
||||
.attr('class', 'progress-indicator')
|
||||
.attr('transform', `translate(${width / 2}, ${-20})`)
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', 'rgba(139, 92, 246, 0.8)')
|
||||
.text(`LTM Progress: ${progress}%`)
|
||||
}
|
||||
|
||||
if (retrievals >= threshold) {
|
||||
const stability = Math.round(ltmFactor * 100)
|
||||
svg.append('text')
|
||||
.attr('class', 'ltm-info')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -10)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '14px')
|
||||
.attr('fill', 'rgba(139, 92, 246, 1)')
|
||||
.text(`Long-term memory stability: ${stability}%`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Setup resize observer
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (props.memoryData.length && props.selectedStoryId) {
|
||||
renderChart()
|
||||
}
|
||||
})
|
||||
|
||||
if (chartContainer.value) {
|
||||
resizeObserver.observe(chartContainer.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
|
||||
<h2 class="mb-4 text-xl font-semibold">
|
||||
Memory Strength Projection
|
||||
</h2>
|
||||
<!-- D3.js chart container -->
|
||||
<div ref="chartContainer" class="relative h-120 w-full">
|
||||
<!-- D3 will render the chart here -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* D3 Chart Styles */
|
||||
:deep(.axis-label) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.x-axis path),
|
||||
:deep(.y-axis path),
|
||||
:deep(.x-axis line),
|
||||
:deep(.y-axis line) {
|
||||
stroke: #dce0e3;
|
||||
}
|
||||
|
||||
:deep(.grid line) {
|
||||
stroke: #2c2d2e;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
|
||||
.dark {
|
||||
:deep(.x-axis path),
|
||||
:deep(.y-axis path),
|
||||
:deep(.x-axis line),
|
||||
:deep(.y-axis line) {
|
||||
stroke: #1f2831;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<!-- src/components/MemoryTable.vue -->
|
||||
<script setup lang="ts">
|
||||
interface MemoryDataItem {
|
||||
storyid: string
|
||||
score: number
|
||||
lastupdate: string // ISO timestamp
|
||||
last_retrieved_at: string // ISO timestamp
|
||||
retrieval_count: number
|
||||
lastupdate_str: string
|
||||
last_retrieved_str: string
|
||||
age_in_seconds: number
|
||||
time_since_retrieval: number
|
||||
ltm_factor?: number // Optional since it's only present when longTermMemoryEnabled is true
|
||||
decayed_score: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
memories: MemoryDataItem[]
|
||||
selectedId: string | null
|
||||
longTermMemoryEnabled: boolean
|
||||
longTermMemoryThreshold: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['select', 'retrieve'])
|
||||
|
||||
function selectMemory(id) {
|
||||
emit('select', id)
|
||||
}
|
||||
|
||||
function retrieveMemory(id, event) {
|
||||
event.stopPropagation()
|
||||
emit('retrieve', id)
|
||||
}
|
||||
|
||||
function getMemoryStatus(memory) {
|
||||
if (props.longTermMemoryEnabled && memory.retrieval_count >= props.longTermMemoryThreshold) {
|
||||
return {
|
||||
type: 'long-term',
|
||||
label: `Long-term (${Math.round((memory.ltm_factor || 0) * 100)}%)`,
|
||||
class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
}
|
||||
}
|
||||
else if (memory.retrieval_count > 0) {
|
||||
return {
|
||||
type: 'working',
|
||||
label: props.longTermMemoryEnabled ? `Working (${memory.retrieval_count}/${props.longTermMemoryThreshold})` : 'Working',
|
||||
class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
type: 'short-term',
|
||||
label: 'Short-term',
|
||||
class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg bg-neutral-50 shadow dark:bg-neutral-800/50">
|
||||
<div class="border-b p-4 dark:border-neutral-700">
|
||||
<h2 class="text-xl font-semibold">
|
||||
All Memory Items
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-neutral-500">
|
||||
Items are ranked by current memory strength (click to analyze or simulate retrieval)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
<thead class="bg-neutral-100 dark:bg-neutral-800">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Rank
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Memory ID
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Original Score
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Age (Days)
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Retrievals
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Last Retrieved
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Memory Status
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Current Score
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-neutral-200 dark:bg-neutral-900 dark:divide-neutral-800">
|
||||
<tr
|
||||
v-for="(item, idx) in memories" :key="item.storyid"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': item.storyid === selectedId }"
|
||||
class="cursor-pointer transition duration-150 hover:bg-neutral-50 dark:hover:bg-neutral-800"
|
||||
@click="selectMemory(item.storyid)"
|
||||
>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm font-medium">
|
||||
{{ idx + 1 }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ item.storyid }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ Math.round(item.score) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ Math.round(item.age_in_seconds / (24 * 60 * 60)) }}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-4 py-2 text-sm font-medium"
|
||||
:class="item.retrieval_count > 0 ? 'text-green-600 dark:text-green-400' : ''"
|
||||
>
|
||||
{{ item.retrieval_count }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ item.retrieval_count > 0 ? new Date(item.last_retrieved_at).toLocaleDateString() : '-' }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
<span
|
||||
class="rounded px-2 py-1 text-xs font-medium"
|
||||
:class="getMemoryStatus(item).class"
|
||||
>
|
||||
{{ getMemoryStatus(item).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<div class="flex items-center">
|
||||
<div class="w-16 text-sm">
|
||||
{{ Math.round(item.decayed_score) }}
|
||||
</div>
|
||||
<div class="ml-2 h-2 w-24 rounded-full bg-neutral-200 dark:bg-neutral-700">
|
||||
<div
|
||||
class="h-full max-w-full rounded-full"
|
||||
:class="longTermMemoryEnabled && item.retrieval_count >= longTermMemoryThreshold ? 'bg-purple-500' : 'bg-blue-500'"
|
||||
:style="`width: ${Math.round((item.decayed_score / item.score) * 100)}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-right">
|
||||
<button
|
||||
class="rounded-lg bg-green-100 px-3 py-1 text-xs font-medium dark:bg-green-900 hover:bg-green-200 dark:hover:bg-green-800"
|
||||
@click="retrieveMemory(item.storyid, $event)"
|
||||
>
|
||||
Retrieve
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media (max-width: 768px) {
|
||||
:deep(table) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
thumbColor?: string
|
||||
trackColor?: string
|
||||
trackValueColor?: string
|
||||
}>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
thumbColor: '#9090906e',
|
||||
trackColor: 'gray',
|
||||
trackValueColor: 'red',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const scaledMin = computed(() => props.min * 10000)
|
||||
const scaledMax = computed(() => props.max * 10000)
|
||||
const scaledStep = computed(() => props.step * 10000)
|
||||
|
||||
const sliderRef = ref<HTMLInputElement>()
|
||||
const sliderValue = ref(props.modelValue * 10000)
|
||||
|
||||
watch(sliderValue, (value) => {
|
||||
emit('update:modelValue', value / 10000)
|
||||
updateTrackColor()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateTrackColor()
|
||||
})
|
||||
|
||||
function updateTrackColor() {
|
||||
if (!sliderRef.value)
|
||||
return
|
||||
|
||||
sliderRef.value.style.setProperty('--value', sliderRef.value.value)
|
||||
sliderRef.value.style.setProperty('--min', !sliderRef.value.min ? props.min.toString() : sliderRef.value.min)
|
||||
sliderRef.value.style.setProperty('--max', !sliderRef.value.max ? props.max.toString() : sliderRef.value.max)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
ref="sliderRef"
|
||||
v-model.number="sliderValue"
|
||||
type="range"
|
||||
:min="scaledMin"
|
||||
:max="scaledMax"
|
||||
:step="scaledStep"
|
||||
class="slider-progress form_input-range"
|
||||
@input="(e) => {
|
||||
(e.target as HTMLInputElement).style.setProperty('--value', (e.target as HTMLInputElement).value)
|
||||
}"
|
||||
>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
.form_input-range {
|
||||
--height: 2em;
|
||||
|
||||
min-height: var(--height);
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
--thumb-width: 4px;
|
||||
--thumb-height: var(--height);
|
||||
--thumb-box-shadow: 0 0 0px #e6e6e6;
|
||||
--thumb-border: none;
|
||||
--thumb-border-radius: 999px;
|
||||
--thumb-background: oklch(80% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-hover: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-active: oklch(70% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
|
||||
--track-height: calc(var(--height) - var(--track-value-padding) * 2);
|
||||
--track-box-shadow: none;
|
||||
--track-border: solid 2px rgb(238, 238, 238);
|
||||
--track-border-radius: 6px;
|
||||
--track-background: rgb(238, 238, 238);
|
||||
--track-background-hover: rgb(238, 238, 238);
|
||||
--track-background-active: rgb(238, 238, 238);
|
||||
|
||||
--track-value-background: rgb(255, 255, 255);
|
||||
--track-value-background-hover: rgb(255, 255, 255);
|
||||
--track-value-background-active: rgb(255, 255, 255);
|
||||
--track-value-padding: 2px;
|
||||
}
|
||||
|
||||
.dark .form_input-range {
|
||||
--thumb-background: oklch(70% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-hover: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-active: oklch(80% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
|
||||
--track-border: solid 2px rgb(44, 44, 44);
|
||||
--track-background: rgb(44, 44, 44);
|
||||
--track-background-hover: rgb(44, 44, 44);
|
||||
--track-background-active: rgb(44, 44, 44);
|
||||
|
||||
--track-value-background: rgb(164, 164, 164);
|
||||
--track-value-background-hover: rgb(164, 164, 164);
|
||||
--track-value-background-active: rgb(164, 164, 164);
|
||||
}
|
||||
|
||||
/*progress support*/
|
||||
.form_input-range.slider-progress {
|
||||
--range: calc(var(--max) - var(--min));
|
||||
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||
--sx: calc(0.5 * 0em + var(--ratio) * (100% - 0em));
|
||||
}
|
||||
|
||||
.form_input-range:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*webkit*/
|
||||
.form_input-range::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: var(--thumb-border);
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
margin-top: calc(var(--track-height) * 0.5 - var(--thumb-height) * 0.5 - 2px);
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
cursor: col-resize;
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out,
|
||||
transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.form_input-range::-webkit-slider-runnable-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
position: relative;
|
||||
cursor: col-resize;
|
||||
transition:
|
||||
box-shadow 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.form_input-range::-webkit-slider-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range:hover::-webkit-slider-runnable-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range::-webkit-slider-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.form_input-range:active::-webkit-slider-runnable-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress::-webkit-slider-runnable-track {
|
||||
/* margin-left: var(--track-value-padding); */
|
||||
margin-right: calc(0 - var(--track-value-padding));
|
||||
background:
|
||||
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:hover::-webkit-slider-runnable-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:active::-webkit-slider-runnable-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-active);
|
||||
}
|
||||
|
||||
/*mozilla*/
|
||||
.form_input-range::-moz-range-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: none;
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
cursor: col-resize;
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
}
|
||||
|
||||
.form_input-range::-moz-range-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
cursor: col-resize;
|
||||
/* Trim left and right paddings of track */
|
||||
width: calc(100% - var(--track-value-padding) * 2);
|
||||
}
|
||||
|
||||
.form_input-range::-moz-range-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range:hover::-moz-range-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range::-moz-range-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.form_input-range:active::-moz-range-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:hover::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:active::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-active);
|
||||
}
|
||||
|
||||
/*ms*/
|
||||
.form_input-range::-ms-fill-upper {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.form_input-range::-ms-fill-lower {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.form_input-range::-ms-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
background: var(--thumb-background);
|
||||
border: var(--thumb-border);
|
||||
box-shadow: var(--thumb-box-shadow);
|
||||
/** Center thumb */
|
||||
margin-top: 0;
|
||||
/** Shift left thumb */
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
box-sizing: border-box;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.form_input-range::-ms-track {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
border: var(--track-border);
|
||||
box-shadow: var(--track-box-shadow);
|
||||
box-sizing: border-box;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.form_input-range::-ms-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range:hover::-ms-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range::-ms-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.form_input-range:active::-ms-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress::-ms-fill-lower {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius) 0 0 var(--track-border-radius);
|
||||
margin: 0;
|
||||
background: var(--track-value-background);
|
||||
border: none;
|
||||
border-right-width: 0;
|
||||
/** Shift left thumb */
|
||||
margin-left: calc(var(--track-value-padding));
|
||||
/** Shift right thumb */
|
||||
margin-right: calc(0 - var(--track-value-padding));
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:hover::-ms-fill-lower {
|
||||
background: var(--track-value-background-hover);
|
||||
}
|
||||
|
||||
.form_input-range.slider-progress:active::-ms-fill-lower {
|
||||
background: var(--track-value-background-active);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
import { buildDSN } from '../../../../src/dsn'
|
||||
import { drizzle } from '../../../../src/index'
|
||||
import * as schema from '../../../db/schema'
|
||||
|
||||
export async function connectToDatabase() {
|
||||
return drizzle(buildDSN({
|
||||
scheme: 'duckdb-wasm:',
|
||||
bundles: 'import-url',
|
||||
logger: true,
|
||||
}), { schema })
|
||||
}
|
||||
|
||||
export async function createSchema(db) {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS currentscores (
|
||||
storyid VARCHAR,
|
||||
score DOUBLE,
|
||||
lastupdate TIMESTAMP,
|
||||
last_retrieved_at TIMESTAMP,
|
||||
retrieval_count INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function loadSampleData(db) {
|
||||
const now = new Date()
|
||||
const sampleData = []
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const daysAgo = Math.random() * 60
|
||||
const lastUpdate = new Date(now.getTime() - (daysAgo * 24 * 60 * 60 * 1000))
|
||||
const retrievalCount = Math.floor(Math.random() * 10)
|
||||
let retrievalDaysAgo = retrievalCount > 0 ? Math.random() * daysAgo * (1 - retrievalCount / 15) : daysAgo
|
||||
retrievalDaysAgo = Math.max(0, retrievalDaysAgo)
|
||||
const lastRetrievedAt = new Date(now.getTime() - (retrievalDaysAgo * 24 * 60 * 60 * 1000))
|
||||
const score = Math.floor(Math.random() * 900) + 100
|
||||
|
||||
sampleData.push({
|
||||
storyid: `story-${i}`,
|
||||
score,
|
||||
lastupdate: lastUpdate.toISOString(),
|
||||
last_retrieved_at: lastRetrievedAt.toISOString(),
|
||||
retrieval_count: retrievalCount,
|
||||
})
|
||||
}
|
||||
|
||||
for (const item of sampleData) {
|
||||
await db.execute(`
|
||||
INSERT INTO currentscores (storyid, score, lastupdate, last_retrieved_at, retrieval_count)
|
||||
VALUES ('${item.storyid}', ${item.score}, '${item.lastupdate}', '${item.last_retrieved_at}', ${item.retrieval_count})
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateDecayQuery(db, {
|
||||
simulatedTimeOffset,
|
||||
decayRate,
|
||||
timeUnitInSeconds,
|
||||
longTermMemoryEnabled,
|
||||
longTermMemoryThreshold,
|
||||
longTermMemoryStability,
|
||||
retrievalBoost,
|
||||
retrievalDecaySlowdown,
|
||||
}) {
|
||||
const simulatedTimestamp = `(CAST(now() AS TIMESTAMP) + INTERVAL '${simulatedTimeOffset} seconds')`
|
||||
|
||||
const ltmFactorClause = longTermMemoryEnabled
|
||||
? `
|
||||
CASE
|
||||
WHEN retrieval_count >= ${longTermMemoryThreshold}
|
||||
THEN 1.0 - ((${longTermMemoryStability}) ^ (retrieval_count / ${longTermMemoryThreshold}))
|
||||
ELSE 0
|
||||
END AS ltm_factor,
|
||||
`
|
||||
: ''
|
||||
|
||||
const ltmDecayModifier = longTermMemoryEnabled
|
||||
? `* (1 - ltm_factor)`
|
||||
: ''
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
storyid,
|
||||
score,
|
||||
lastupdate,
|
||||
last_retrieved_at,
|
||||
retrieval_count,
|
||||
CAST(lastupdate AS VARCHAR) as lastupdate_str,
|
||||
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str,
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - lastupdate))) as age_in_seconds,
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at))) as time_since_retrieval,
|
||||
${ltmFactorClause}
|
||||
score *
|
||||
exp(-${decayRate} * (EXTRACT(EPOCH FROM (${simulatedTimestamp} - lastupdate)))/(${timeUnitInSeconds}) ${ltmDecayModifier}) *
|
||||
(1 + (retrieval_count * ${retrievalBoost} *
|
||||
exp(-${decayRate * retrievalDecaySlowdown} *
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds}))))
|
||||
as decayed_score
|
||||
FROM currentscores
|
||||
ORDER BY decayed_score DESC
|
||||
`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export async function simulateRetrieval(db, storyId, simulatedTime) {
|
||||
const simulatedTimestamp = simulatedTime.toISOString()
|
||||
|
||||
await db.execute(`
|
||||
UPDATE currentscores
|
||||
SET last_retrieved_at = '${simulatedTimestamp}',
|
||||
retrieval_count = retrieval_count + 1
|
||||
WHERE storyid = '${storyId}'
|
||||
`)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { routes } from 'vue-router/auto-routes'
|
||||
import App from './App.vue'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
||||
import './styles/themes.css'
|
||||
|
||||
const router = createRouter({ routes, history: createWebHashHistory() })
|
||||
|
||||
|
||||
@@ -728,7 +728,7 @@ onUnmounted(() => {
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm font-medium">Query Type</label>
|
||||
<select v-model="queryType" class="w-full border rounded p-2">
|
||||
<select v-model="queryType" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
|
||||
<option value="recursive">
|
||||
Recursive (All Paths)
|
||||
</option>
|
||||
@@ -743,7 +743,7 @@ onUnmounted(() => {
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm font-medium">Start Node</label>
|
||||
<select v-model="selectedStartNode" class="w-full border rounded p-2">
|
||||
<select v-model="selectedStartNode" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
|
||||
<option v-for="node in graphData.nodes" :key="`start-${node.id}`" :value="node.id">
|
||||
{{ node.name }} ({{ node.type }})
|
||||
</option>
|
||||
@@ -752,7 +752,7 @@ onUnmounted(() => {
|
||||
|
||||
<div v-if="queryType === 'path'" class="mb-4">
|
||||
<label class="mb-1 block text-sm font-medium">End Node</label>
|
||||
<select v-model="selectedEndNode" class="w-full border rounded p-2">
|
||||
<select v-model="selectedEndNode" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
|
||||
<option v-for="node in graphData.nodes" :key="`end-${node.id}`" :value="node.id">
|
||||
{{ node.name }} ({{ node.type }})
|
||||
</option>
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
<!-- src/MemoryDecaySimulator.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import MemoryModelSettings from '../components/Memory/DecayModelSettings.vue'
|
||||
import TimeControls from '../components/Memory/DecayTimeSettings.vue'
|
||||
import MemoryDetails from '../components/Memory/RecordDetail.vue'
|
||||
import MemoryChart from '../components/Memory/VisualizeChart.vue'
|
||||
import MemoryTable from '../components/Memory/VisualizeTable.vue'
|
||||
import { connectToDatabase, createSchema, generateDecayQuery, loadSampleData, simulateRetrieval } from '../composables/memory/db'
|
||||
|
||||
interface MemoryDataItem {
|
||||
storyid: string
|
||||
score: number
|
||||
lastupdate: string // ISO timestamp
|
||||
last_retrieved_at: string // ISO timestamp
|
||||
retrieval_count: number
|
||||
lastupdate_str: string
|
||||
last_retrieved_str: string
|
||||
age_in_seconds: number
|
||||
time_since_retrieval: number
|
||||
ltm_factor?: number // Optional since it's only present when longTermMemoryEnabled is true
|
||||
decayed_score: number
|
||||
}
|
||||
|
||||
// Database references
|
||||
const db = ref(null)
|
||||
const isMigrated = ref(false)
|
||||
|
||||
// Data and query results
|
||||
const rawData = ref<MemoryDataItem[]>([])
|
||||
const decayedResults = ref<MemoryDataItem[]>([])
|
||||
|
||||
// Parameters for decay function
|
||||
const decayRate = ref(0.0990)
|
||||
const timeUnit = ref('days')
|
||||
const maxDaysToShow = ref(30)
|
||||
const selectedStoryId = ref(null)
|
||||
|
||||
// Time acceleration parameters
|
||||
const timeMultiplier = ref(60 * 60 * 24) // Default: 1 day/second
|
||||
const isTimeAccelerated = ref(false)
|
||||
const simulatedTimeOffset = ref(0)
|
||||
const lastTickTime = ref(Date.now())
|
||||
|
||||
// Advanced memory model parameters
|
||||
const retrievalBoost = ref(0.25)
|
||||
const retrievalDecaySlowdown = ref(0.8)
|
||||
const longTermMemoryEnabled = ref(true)
|
||||
const longTermMemoryThreshold = ref(5)
|
||||
const longTermMemoryStability = ref(0.3)
|
||||
const longTermMemoryVisualize = ref(true)
|
||||
|
||||
// UI elements
|
||||
const showInfoPanel = ref(false)
|
||||
|
||||
// Selected memory
|
||||
const selectedMemory = computed(() => {
|
||||
if (!selectedStoryId.value || !decayedResults.value.length)
|
||||
return null
|
||||
return decayedResults.value.find(s => s.storyid === selectedStoryId.value)
|
||||
})
|
||||
|
||||
// Time unit in seconds
|
||||
const timeUnitInSeconds = computed(() => {
|
||||
switch (timeUnit.value) {
|
||||
case 'hours': return 60 * 60
|
||||
case 'days': return 24 * 60 * 60
|
||||
case 'weeks': return 7 * 24 * 60 * 60
|
||||
case 'months': return 30 * 24 * 60 * 60
|
||||
default: return 24 * 60 * 60
|
||||
}
|
||||
})
|
||||
|
||||
// Current simulated time
|
||||
const currentSimulatedTime = computed(() => {
|
||||
const now = new Date()
|
||||
now.setSeconds(now.getSeconds() + simulatedTimeOffset.value)
|
||||
return now
|
||||
})
|
||||
|
||||
// Initialize database and load data
|
||||
async function initialize() {
|
||||
db.value = await connectToDatabase()
|
||||
await createSchema(db.value)
|
||||
await loadSampleData(db.value)
|
||||
isMigrated.value = true
|
||||
await loadData()
|
||||
}
|
||||
|
||||
// Load data from the database
|
||||
async function loadData() {
|
||||
rawData.value = await db.value?.execute(`
|
||||
SELECT storyid, score, lastupdate, last_retrieved_at, retrieval_count,
|
||||
CAST(lastupdate AS VARCHAR) as lastupdate_str,
|
||||
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str
|
||||
FROM currentscores
|
||||
ORDER BY score DESC
|
||||
`) || []
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
const decayQuery = ref('')
|
||||
|
||||
watch([simulatedTimeOffset, decayRate, timeUnit, longTermMemoryEnabled, longTermMemoryThreshold, longTermMemoryStability, retrievalBoost, retrievalDecaySlowdown], async () => {
|
||||
const query = await generateDecayQuery(db.value, {
|
||||
simulatedTimeOffset: simulatedTimeOffset.value,
|
||||
decayRate: decayRate.value,
|
||||
timeUnitInSeconds: timeUnitInSeconds.value,
|
||||
longTermMemoryEnabled: longTermMemoryEnabled.value,
|
||||
longTermMemoryThreshold: longTermMemoryThreshold.value,
|
||||
longTermMemoryStability: longTermMemoryStability.value,
|
||||
retrievalBoost: retrievalBoost.value,
|
||||
retrievalDecaySlowdown: retrievalDecaySlowdown.value,
|
||||
})
|
||||
|
||||
decayQuery.value = query
|
||||
})
|
||||
|
||||
// Run the decay query
|
||||
async function runDecayQuery() {
|
||||
if (!decayQuery.value)
|
||||
return
|
||||
|
||||
decayedResults.value = await db.value?.execute(decayQuery.value) || []
|
||||
|
||||
// Initialize selectedStoryId if not set or update if selection changed
|
||||
if (!selectedStoryId.value && decayedResults.value.length) {
|
||||
selectedStoryId.value = decayedResults.value[0].storyid
|
||||
}
|
||||
}
|
||||
|
||||
// Handle simulated retrieval
|
||||
async function handleRetrieval(storyId) {
|
||||
await simulateRetrieval(db.value, storyId, currentSimulatedTime.value)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
// Handle time jump
|
||||
async function handleTimeJump({ amount, unit }) {
|
||||
let secondsToAdd = 0
|
||||
|
||||
switch (unit) {
|
||||
case 'hour':
|
||||
secondsToAdd = 60 * 60
|
||||
break
|
||||
case 'day':
|
||||
secondsToAdd = 24 * 60 * 60
|
||||
break
|
||||
case 'week':
|
||||
secondsToAdd = 7 * 24 * 60 * 60
|
||||
break
|
||||
case 'month':
|
||||
secondsToAdd = 30 * 24 * 60 * 60
|
||||
break
|
||||
}
|
||||
|
||||
simulatedTimeOffset.value += amount * secondsToAdd
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// Toggle info panel
|
||||
function toggleInfoPanel() {
|
||||
showInfoPanel.value = !showInfoPanel.value
|
||||
}
|
||||
|
||||
// Time acceleration handling
|
||||
function tickTime() {
|
||||
if (!isTimeAccelerated.value)
|
||||
return
|
||||
|
||||
const now = Date.now()
|
||||
const elapsedMs = now - lastTickTime.value
|
||||
simulatedTimeOffset.value += (elapsedMs / 1000) * timeMultiplier.value
|
||||
lastTickTime.value = now
|
||||
|
||||
runDecayQuery()
|
||||
requestAnimationFrame(tickTime)
|
||||
}
|
||||
|
||||
// Watch for parameter changes
|
||||
watch([
|
||||
decayRate,
|
||||
timeUnit,
|
||||
maxDaysToShow,
|
||||
retrievalBoost,
|
||||
retrievalDecaySlowdown,
|
||||
longTermMemoryEnabled,
|
||||
longTermMemoryThreshold,
|
||||
longTermMemoryStability,
|
||||
longTermMemoryVisualize,
|
||||
], async () => {
|
||||
if (isMigrated.value) {
|
||||
await runDecayQuery()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch time acceleration state
|
||||
watch(isTimeAccelerated, (newValue) => {
|
||||
if (newValue) {
|
||||
lastTickTime.value = Date.now()
|
||||
tickTime()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch time multiplier changes
|
||||
watch(timeMultiplier, () => {
|
||||
if (isTimeAccelerated.value) {
|
||||
lastTickTime.value = Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
isTimeAccelerated.value = true
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isTimeAccelerated.value = false
|
||||
db.value?.$client.then(client => client.close())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl p-4">
|
||||
<!-- Loading state -->
|
||||
<div v-if="!isMigrated" class="py-8 text-center">
|
||||
<div class="mx-auto h-8 w-8 animate-spin border-4 border-blue-500 border-t-transparent rounded-full" />
|
||||
<p class="mt-4">
|
||||
Initializing database and sample data...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<header class="mb-6">
|
||||
<h1 class="mb-2 text-2xl font-bold">
|
||||
Memory Decay & Retention Simulator
|
||||
</h1>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="max-w-2xl text-neutral-600 dark:text-neutral-300">
|
||||
Visualize how memories decay over time and how repeated retrievals create long-term memory
|
||||
<button class="ml-2 text-blue-500 hover:underline" @click="toggleInfoPanel">
|
||||
{{ showInfoPanel ? 'Hide info' : 'Learn more' }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Info Panel -->
|
||||
<div v-if="showInfoPanel" class="mb-6 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-700/20">
|
||||
<h2 class="mb-2 text-lg font-semibold">
|
||||
About Memory Decay and Long-Term Memory Formation
|
||||
</h2>
|
||||
<p class="mb-2">
|
||||
This simulator models both the forgetting curve and how memories become more stable with repeated retrievals:
|
||||
</p>
|
||||
<div class="my-3 rounded bg-white p-3 text-center font-mono dark:bg-neutral-800">
|
||||
score * exp(-decay_rate * time_elapsed / time_unit * (1 - ltm_factor)) * (1 + retrieval_boost)
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
The simulator models three key memory phenomena:
|
||||
</p>
|
||||
<ol class="mt-2 list-decimal pl-5 space-y-1">
|
||||
<li>
|
||||
<strong>Exponential decay</strong>: Memories naturally fade over time following Ebbinghaus' forgetting curve
|
||||
</li>
|
||||
<li>
|
||||
<strong>Retrieval practice effect</strong>: Each retrieval boosts the memory strength temporarily
|
||||
</li>
|
||||
<li>
|
||||
<strong>Long-term memory formation</strong>: After sufficient retrievals, memories become increasingly stable
|
||||
and resistant to decay, eventually becoming "permanent"
|
||||
</li>
|
||||
</ol>
|
||||
<p class="mt-3">
|
||||
This reflects how the brain forms long-term memories through a process of neural consolidation,
|
||||
where repeated activation of neural pathways leads to structural changes that stabilize memories.
|
||||
The spaced repetition study method leverages this effect for efficient learning.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Chart Section -->
|
||||
<div class="grid grid-cols-1 mb-4 gap-4 lg:grid-cols-3">
|
||||
<!-- Chart -->
|
||||
<MemoryChart
|
||||
:memory-data="decayedResults"
|
||||
:selected-story-id="selectedStoryId"
|
||||
:decay-rate="decayRate"
|
||||
:max-days-to-show="maxDaysToShow"
|
||||
:long-term-memory-enabled="longTermMemoryEnabled"
|
||||
:long-term-memory-threshold="longTermMemoryThreshold"
|
||||
:long-term-memory-visualize="longTermMemoryVisualize"
|
||||
:retrieval-boost="retrievalBoost"
|
||||
:retrieval-decay-slowdown="retrievalDecaySlowdown"
|
||||
class="lg:col-span-2"
|
||||
/>
|
||||
|
||||
<!-- Selected Memory Details -->
|
||||
<MemoryDetails
|
||||
v-if="selectedMemory"
|
||||
:memory="selectedMemory"
|
||||
:long-term-memory-enabled="longTermMemoryEnabled"
|
||||
:long-term-memory-threshold="longTermMemoryThreshold"
|
||||
@retrieve="handleRetrieval"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Time Controls -->
|
||||
<TimeControls
|
||||
v-model:simulated-time-offset="simulatedTimeOffset"
|
||||
v-model:is-time-accelerated="isTimeAccelerated"
|
||||
v-model:time-multiplier="timeMultiplier"
|
||||
class="mb-4"
|
||||
@time-jump="handleTimeJump"
|
||||
/>
|
||||
|
||||
<!-- Memory Model Settings -->
|
||||
<MemoryModelSettings
|
||||
v-model:long-term-memory-enabled="longTermMemoryEnabled"
|
||||
v-model:long-term-memory-threshold="longTermMemoryThreshold"
|
||||
v-model:long-term-memory-stability="longTermMemoryStability"
|
||||
v-model:long-term-memory-visualize="longTermMemoryVisualize"
|
||||
v-model:retrieval-boost="retrievalBoost"
|
||||
v-model:retrieval-decay-slowdown="retrievalDecaySlowdown"
|
||||
v-model:decay-rate="decayRate"
|
||||
v-model:time-unit="timeUnit"
|
||||
v-model:max-days-to-show="maxDaysToShow"
|
||||
/>
|
||||
|
||||
<!-- Memory Table -->
|
||||
<MemoryTable
|
||||
:memories="decayedResults"
|
||||
:selected-id="selectedStoryId"
|
||||
:long-term-memory-enabled="longTermMemoryEnabled"
|
||||
:long-term-memory-threshold="longTermMemoryThreshold"
|
||||
class="mb-4"
|
||||
@select="selectedStoryId = $event"
|
||||
@retrieve="handleRetrieval"
|
||||
/>
|
||||
|
||||
<!-- SQL Query Preview -->
|
||||
<div class="max-w-screen-lg overflow-x-scroll rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800">
|
||||
<h2 class="mb-2 text-xl font-semibold">
|
||||
Current SQL Query
|
||||
</h2>
|
||||
<pre class="overflow-x-auto rounded bg-neutral-800 p-4 text-sm text-neutral-200">{{ decayQuery }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
:root {
|
||||
--theme-colors-hue: 255;
|
||||
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
|
||||
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
|
||||
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
|
||||
--theme-colors-chroma-200: calc(var(--theme-colors-chroma) * 0.6);
|
||||
--theme-colors-chroma-300: calc(var(--theme-colors-chroma) * 0.75);
|
||||
--theme-colors-chroma-400: var(--theme-colors-chroma);
|
||||
--theme-colors-chroma-600: calc(var(--theme-colors-chroma) * 1.15);
|
||||
--theme-colors-chroma-700: calc(var(--theme-colors-chroma) * 1.1);
|
||||
--theme-colors-chroma-800: calc(var(--theme-colors-chroma) * 0.85);
|
||||
--theme-colors-chroma-900: calc(var(--theme-colors-chroma) * 0.7);
|
||||
--theme-colors-chroma-950: calc(var(--theme-colors-chroma) * 0.5);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ function updateTrackColor() {
|
||||
:min="scaledMin"
|
||||
:max="scaledMax"
|
||||
:step="scaledStep"
|
||||
class="data-gui-range slider-progress"
|
||||
class="form_input-range slider-progress"
|
||||
@input="(e) => {
|
||||
(e.target as HTMLInputElement).style.setProperty('--value', (e.target as HTMLInputElement).value)
|
||||
}"
|
||||
@@ -68,7 +68,7 @@ function updateTrackColor() {
|
||||
<style scoped>
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
.data-gui-range {
|
||||
.form_input-range {
|
||||
--height: 2em;
|
||||
|
||||
min-height: var(--height);
|
||||
@@ -100,7 +100,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
--track-value-padding: 2px;
|
||||
}
|
||||
|
||||
.dark .data-gui-range {
|
||||
.dark .form_input-range {
|
||||
--thumb-background: oklch(70% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-hover: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
--thumb-background-active: oklch(80% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
|
||||
@@ -116,18 +116,18 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
}
|
||||
|
||||
/*progress support*/
|
||||
.data-gui-range.slider-progress {
|
||||
.form_input-range.slider-progress {
|
||||
--range: calc(var(--max) - var(--min));
|
||||
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||
--sx: calc(0.5 * 0em + var(--ratio) * (100% - 0em));
|
||||
}
|
||||
|
||||
.data-gui-range:focus {
|
||||
.form_input-range:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*webkit*/
|
||||
.data-gui-range::-webkit-slider-thumb {
|
||||
.form_input-range::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
@@ -145,7 +145,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.data-gui-range::-webkit-slider-runnable-track {
|
||||
.form_input-range::-webkit-slider-runnable-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
@@ -158,23 +158,23 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.data-gui-range::-webkit-slider-thumb:hover {
|
||||
.form_input-range::-webkit-slider-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range:hover::-webkit-slider-runnable-track {
|
||||
.form_input-range:hover::-webkit-slider-runnable-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range::-webkit-slider-thumb:active {
|
||||
.form_input-range::-webkit-slider-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range:active::-webkit-slider-runnable-track {
|
||||
.form_input-range:active::-webkit-slider-runnable-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress::-webkit-slider-runnable-track {
|
||||
.form_input-range.slider-progress::-webkit-slider-runnable-track {
|
||||
/* margin-left: var(--track-value-padding); */
|
||||
margin-right: calc(0 - var(--track-value-padding));
|
||||
background:
|
||||
@@ -182,14 +182,14 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:hover::-webkit-slider-runnable-track {
|
||||
.form_input-range.slider-progress:hover::-webkit-slider-runnable-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:active::-webkit-slider-runnable-track {
|
||||
.form_input-range.slider-progress:active::-webkit-slider-runnable-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
@@ -197,7 +197,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
}
|
||||
|
||||
/*mozilla*/
|
||||
.data-gui-range::-moz-range-thumb {
|
||||
.form_input-range::-moz-range-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
@@ -208,7 +208,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
margin-left: calc(0 - var(--track-value-padding));
|
||||
}
|
||||
|
||||
.data-gui-range::-moz-range-track {
|
||||
.form_input-range::-moz-range-track {
|
||||
height: var(--track-height);
|
||||
border: var(--track-border);
|
||||
border-radius: var(--track-border-radius);
|
||||
@@ -219,36 +219,36 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
width: calc(100% - var(--track-value-padding) * 2);
|
||||
}
|
||||
|
||||
.data-gui-range::-moz-range-thumb:hover {
|
||||
.form_input-range::-moz-range-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range:hover::-moz-range-track {
|
||||
.form_input-range:hover::-moz-range-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range::-moz-range-thumb:active {
|
||||
.form_input-range::-moz-range-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range:active::-moz-range-track {
|
||||
.form_input-range:active::-moz-range-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress::-moz-range-track {
|
||||
.form_input-range.slider-progress::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
|
||||
var(--track-background);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:hover::-moz-range-track {
|
||||
.form_input-range.slider-progress:hover::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
var(--track-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:active::-moz-range-track {
|
||||
.form_input-range.slider-progress:active::-moz-range-track {
|
||||
background:
|
||||
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
|
||||
no-repeat,
|
||||
@@ -256,17 +256,17 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
}
|
||||
|
||||
/*ms*/
|
||||
.data-gui-range::-ms-fill-upper {
|
||||
.form_input-range::-ms-fill-upper {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.data-gui-range::-ms-fill-lower {
|
||||
.form_input-range::-ms-fill-lower {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.data-gui-range::-ms-thumb {
|
||||
.form_input-range::-ms-thumb {
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border-radius: var(--thumb-border-radius);
|
||||
@@ -281,7 +281,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.data-gui-range::-ms-track {
|
||||
.form_input-range::-ms-track {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius);
|
||||
background: var(--track-background);
|
||||
@@ -291,23 +291,23 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.data-gui-range::-ms-thumb:hover {
|
||||
.form_input-range::-ms-thumb:hover {
|
||||
background: var(--thumb-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range:hover::-ms-track {
|
||||
.form_input-range:hover::-ms-track {
|
||||
background: var(--track-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range::-ms-thumb:active {
|
||||
.form_input-range::-ms-thumb:active {
|
||||
background: var(--thumb-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range:active::-ms-track {
|
||||
.form_input-range:active::-ms-track {
|
||||
background: var(--track-background-active);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress::-ms-fill-lower {
|
||||
.form_input-range.slider-progress::-ms-fill-lower {
|
||||
height: var(--track-height);
|
||||
border-radius: var(--track-border-radius) 0 0 var(--track-border-radius);
|
||||
margin: 0;
|
||||
@@ -320,11 +320,11 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||
margin-right: calc(0 - var(--track-value-padding));
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:hover::-ms-fill-lower {
|
||||
.form_input-range.slider-progress:hover::-ms-fill-lower {
|
||||
background: var(--track-value-background-hover);
|
||||
}
|
||||
|
||||
.data-gui-range.slider-progress:active::-ms-fill-lower {
|
||||
.form_input-range.slider-progress:active::-ms-fill-lower {
|
||||
background: var(--track-value-background-active);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user