feat(drizzle-duckdb-wasm): recall & emotion
This commit is contained in:
+29
-27
@@ -101,31 +101,31 @@ function jumpAhead(amount, unit) {
|
||||
|
||||
<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>
|
||||
<h2 class="flex items-center text-lg font-semibold" gap-4>
|
||||
<div flex-1>
|
||||
Time Simulation
|
||||
</div>
|
||||
<div class="text-sm font-mono">
|
||||
{{ currentSimulatedTime }}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<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:pause-bold />
|
||||
<div v-else i-solar:play-bold />
|
||||
</button>
|
||||
|
||||
<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:pause-bold />
|
||||
<div v-else i-solar:play-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>
|
||||
<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>
|
||||
</h2>
|
||||
|
||||
<!-- Time jump shortcuts -->
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@@ -158,12 +158,14 @@ function jumpAhead(amount, unit) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 text-lg font-semibold">
|
||||
Speed
|
||||
<h2 class="mt-4 flex items-center text-lg font-semibold">
|
||||
<div flex-1>
|
||||
Speed
|
||||
</div>
|
||||
<div class="text-sm font-mono">
|
||||
{{ formattedTimeMultiplier }}
|
||||
</div>
|
||||
</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">
|
||||
|
||||
+607
@@ -0,0 +1,607 @@
|
||||
<script setup lang="ts">
|
||||
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
|
||||
|
||||
import { useDark } from '@vueuse/core'
|
||||
import * as d3 from 'd3'
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memoryData: EmotionalMemoryItem[]
|
||||
selectedMemoryId: string
|
||||
decayRate: number
|
||||
maxDaysToProject: number
|
||||
longTermThreshold: number
|
||||
muscleMemoryThreshold: number
|
||||
joyBoostFactor: number
|
||||
joyDecaySteepness: number
|
||||
aversionSpikeFactor: number
|
||||
aversionStability: number
|
||||
}>()
|
||||
|
||||
interface DataPoint {
|
||||
x: number
|
||||
y: number
|
||||
label: string
|
||||
type: string
|
||||
}
|
||||
|
||||
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.selectedMemoryId) {
|
||||
renderChart()
|
||||
}
|
||||
})
|
||||
|
||||
// Render D3 chart
|
||||
function renderChart() {
|
||||
// Clear existing chart
|
||||
d3.select(chartContainer.value).selectAll('*').remove()
|
||||
|
||||
// Find the selected memory
|
||||
const memory = props.memoryData.find(m => m.id === props.selectedMemoryId)
|
||||
if (!memory)
|
||||
return
|
||||
|
||||
// Get container dimensions
|
||||
const containerRect = chartContainer.value.getBoundingClientRect()
|
||||
const containerWidth = containerRect.width
|
||||
const containerHeight = containerRect.height || 700
|
||||
|
||||
// Set chart margins
|
||||
const margin = { top: 40, right: 30, bottom: 40, left: 60 }
|
||||
const width = containerWidth - margin.left - margin.right
|
||||
const height = containerHeight - margin.top - margin.bottom
|
||||
|
||||
// Prepare data for chart
|
||||
const originalScore = Number.parseFloat(String(memory.score))
|
||||
const joyScore = Number.parseFloat(String(memory.joy_score))
|
||||
const aversionScore = Number.parseFloat(String(memory.aversion_score))
|
||||
const retrievalCount = Number.parseInt(String(memory.retrieval_count))
|
||||
const dr = props.decayRate
|
||||
const maxDays = props.maxDaysToProject
|
||||
const ageInDays = Number.parseFloat(String(memory.age_in_seconds)) / (24 * 60 * 60)
|
||||
const timeSinceRetrievalDays = Number.parseFloat(String(memory.time_since_retrieval)) / (24 * 60 * 60)
|
||||
|
||||
// Calculate LTM factor
|
||||
const ltmFactor = retrievalCount >= props.longTermThreshold
|
||||
? 1.0 - (0.3 ** (retrievalCount / props.longTermThreshold))
|
||||
: 0
|
||||
|
||||
// Calculate current score components
|
||||
const currentJoyEffect = joyScore * props.joyBoostFactor
|
||||
* Math.exp(-props.joyDecaySteepness * timeSinceRetrievalDays)
|
||||
|
||||
const currentAversionEffect = aversionScore * props.aversionSpikeFactor
|
||||
* props.aversionStability ** Math.ceil(retrievalCount / 5)
|
||||
|
||||
// Generate baseline data (standard decay without emotional effect)
|
||||
const baselineData = [{
|
||||
x: 0,
|
||||
y: originalScore,
|
||||
label: 'Original',
|
||||
type: 'baseline',
|
||||
}]
|
||||
|
||||
// Generate data with emotional effects
|
||||
const emotionalData = [{
|
||||
x: 0,
|
||||
y: originalScore,
|
||||
label: 'Original',
|
||||
type: 'emotional',
|
||||
}]
|
||||
|
||||
// Generate joy component data
|
||||
const joyData = [{
|
||||
x: 0,
|
||||
y: joyScore * props.joyBoostFactor * originalScore,
|
||||
label: 'Original Joy',
|
||||
type: 'joy',
|
||||
}]
|
||||
|
||||
// Generate aversion component data
|
||||
const aversionData = [{
|
||||
x: 0,
|
||||
y: aversionScore * props.aversionSpikeFactor * originalScore,
|
||||
label: 'Original Aversion',
|
||||
type: 'aversion',
|
||||
}]
|
||||
|
||||
// Current point (today)
|
||||
baselineData.push({
|
||||
x: ageInDays,
|
||||
y: originalScore * Math.exp(-dr * ageInDays * (1 - ltmFactor)),
|
||||
label: 'Current',
|
||||
type: 'baseline',
|
||||
})
|
||||
|
||||
emotionalData.push({
|
||||
x: ageInDays,
|
||||
y: Number.parseFloat(String(memory.decayed_score)),
|
||||
label: 'Current',
|
||||
type: 'emotional',
|
||||
})
|
||||
|
||||
joyData.push({
|
||||
x: ageInDays,
|
||||
y: originalScore * currentJoyEffect,
|
||||
label: 'Current Joy',
|
||||
type: 'joy',
|
||||
})
|
||||
|
||||
aversionData.push({
|
||||
x: ageInDays,
|
||||
y: originalScore * currentAversionEffect,
|
||||
label: 'Current Aversion',
|
||||
type: 'aversion',
|
||||
})
|
||||
|
||||
// Add future projection points
|
||||
const dayStep = Math.ceil(maxDays / 20) // Adjust steps based on projection days
|
||||
for (let day = dayStep; day <= maxDays; day += dayStep) {
|
||||
const projectedDay = ageInDays + day
|
||||
const projectedDaysSinceRetrieval = timeSinceRetrievalDays + day
|
||||
const label = `+${day}d`
|
||||
|
||||
// Calculate LTM-adjusted decay
|
||||
const baseDecay = Math.exp(-dr * projectedDay * (1 - ltmFactor))
|
||||
|
||||
// Joy decays more quickly over time
|
||||
const joyEffect = joyScore * props.joyBoostFactor
|
||||
* Math.exp(-props.joyDecaySteepness * projectedDaysSinceRetrieval)
|
||||
|
||||
// Aversion is more stable, especially with repeated retrievals
|
||||
const aversionEffect = aversionScore * props.aversionSpikeFactor
|
||||
* props.aversionStability ** Math.ceil(retrievalCount / 5)
|
||||
|
||||
// Combined emotional effect
|
||||
const emotionalEffect = (1 + joyEffect) * (1 + aversionEffect)
|
||||
|
||||
// Add baseline point (standard decay)
|
||||
baselineData.push({
|
||||
x: projectedDay,
|
||||
y: originalScore * baseDecay,
|
||||
label,
|
||||
type: 'baseline',
|
||||
})
|
||||
|
||||
// Add emotional point (with joy and aversion effects)
|
||||
emotionalData.push({
|
||||
x: projectedDay,
|
||||
y: originalScore * baseDecay * emotionalEffect,
|
||||
label,
|
||||
type: 'emotional',
|
||||
})
|
||||
|
||||
// Add joy component (isolates joy effect)
|
||||
joyData.push({
|
||||
x: projectedDay,
|
||||
y: originalScore * joyEffect,
|
||||
label,
|
||||
type: 'joy',
|
||||
})
|
||||
|
||||
// Add aversion component (isolates aversion effect)
|
||||
aversionData.push({
|
||||
x: projectedDay,
|
||||
y: originalScore * aversionEffect,
|
||||
label,
|
||||
type: 'aversion',
|
||||
})
|
||||
}
|
||||
|
||||
// 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([
|
||||
...emotionalData,
|
||||
...baselineData,
|
||||
...joyData,
|
||||
...aversionData,
|
||||
], 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
|
||||
addDataLines(svg, baselineData, emotionalData, joyData, aversionData, xScale, yScale)
|
||||
|
||||
// Add key threshold markers
|
||||
if (retrievalCount > 0) {
|
||||
const halfLife = Math.log(2) / dr
|
||||
if (halfLife <= maxDays + ageInDays) {
|
||||
addThresholdLine(svg, halfLife, xScale, height, 'Half-life', 'rgba(220, 38, 38, 0.6)')
|
||||
}
|
||||
}
|
||||
|
||||
// Add memory type marker
|
||||
let memoryTypeLabel = 'Short-term Memory'
|
||||
let memoryTypeColor = 'rgba(220, 38, 38, 0.8)'
|
||||
|
||||
if (retrievalCount >= props.muscleMemoryThreshold) {
|
||||
memoryTypeLabel = 'Muscle Memory'
|
||||
memoryTypeColor = 'rgba(139, 92, 246, 0.8)'
|
||||
}
|
||||
else if (retrievalCount >= props.longTermThreshold) {
|
||||
memoryTypeLabel = 'Long-term Memory'
|
||||
memoryTypeColor = 'rgba(59, 130, 246, 0.8)'
|
||||
}
|
||||
else if (retrievalCount > 0) {
|
||||
memoryTypeLabel = 'Working Memory'
|
||||
memoryTypeColor = 'rgba(14, 165, 233, 0.8)'
|
||||
}
|
||||
|
||||
addMemoryTypeMarker(svg, memoryTypeLabel, memoryTypeColor, retrievalCount, width)
|
||||
|
||||
// Add chart title and legends
|
||||
addTitleAndLegends(svg, memory.id, joyScore, aversionScore, retrievalCount, width)
|
||||
|
||||
// Add data points with interaction
|
||||
addDataPoints(svg, emotionalData, baselineData, xScale, yScale)
|
||||
}
|
||||
|
||||
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
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(
|
||||
d3.axisBottom(xScale)
|
||||
.tickSize(-height)
|
||||
.tickFormat(() => ''),
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.7)')
|
||||
.attr('stroke-width', 1)
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'grid')
|
||||
.call(
|
||||
d3.axisLeft(yScale)
|
||||
.tickSize(-width)
|
||||
.tickFormat(() => ''),
|
||||
)
|
||||
.selectAll('line')
|
||||
.attr('stroke', isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.7)')
|
||||
.attr('stroke-width', 1)
|
||||
|
||||
// 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', 36)
|
||||
.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', -40)
|
||||
.attr('x', -height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text('Memory Strength')
|
||||
.attr('fill', 'currentColor')
|
||||
}
|
||||
|
||||
function addDataLines(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
baselineData: DataPoint[],
|
||||
emotionalData: DataPoint[],
|
||||
joyData: DataPoint[],
|
||||
aversionData: DataPoint[],
|
||||
xScale: d3.ScaleLinear<number, number>,
|
||||
yScale: d3.ScaleLinear<number, number>,
|
||||
): void {
|
||||
// Create line generator
|
||||
const line = d3.line<DataPoint>()
|
||||
.x(d => xScale(d.x))
|
||||
.y(d => yScale(d.y))
|
||||
.curve(d3.curveMonotoneX)
|
||||
|
||||
// Add baseline decay line
|
||||
svg.append('path')
|
||||
.datum(baselineData)
|
||||
.attr('class', 'line baseline')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(156, 163, 175, 0.8)')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,3')
|
||||
|
||||
// Add joy component line (transparent fill under curve)
|
||||
svg.append('path')
|
||||
.datum(joyData)
|
||||
.attr('class', 'line joy')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(250, 204, 21, 0.8)')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '3,2')
|
||||
|
||||
// Add aversion component line (transparent fill under curve)
|
||||
svg.append('path')
|
||||
.datum(aversionData)
|
||||
.attr('class', 'line aversion')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(220, 38, 38, 0.8)')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '3,2')
|
||||
|
||||
// Add emotional effect line (main result)
|
||||
svg.append('path')
|
||||
.datum(emotionalData)
|
||||
.attr('class', 'line emotional')
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(59, 130, 246, 1)')
|
||||
.attr('stroke-width', 3)
|
||||
}
|
||||
|
||||
function addThresholdLine(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
position: number,
|
||||
xScale: d3.ScaleLinear<number, number>,
|
||||
height: number,
|
||||
label: string,
|
||||
color: string,
|
||||
): void {
|
||||
svg.append('line')
|
||||
.attr('class', 'threshold-line')
|
||||
.attr('x1', xScale(position))
|
||||
.attr('y1', 0)
|
||||
.attr('x2', xScale(position))
|
||||
.attr('y2', height)
|
||||
.attr('stroke', color)
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke-dasharray', '5,5')
|
||||
|
||||
svg.append('text')
|
||||
.attr('class', 'threshold-label')
|
||||
.attr('x', xScale(position))
|
||||
.attr('y', -8)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', color)
|
||||
.text(`${label}: ${position.toFixed(1)} days`)
|
||||
}
|
||||
|
||||
function addMemoryTypeMarker(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
typeLabel: string,
|
||||
color: string,
|
||||
retrievalCount: number,
|
||||
width: number,
|
||||
): void {
|
||||
svg.append('text')
|
||||
.attr('class', 'memory-type-label')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '14px')
|
||||
.attr('fill', color)
|
||||
.text(`${typeLabel} (${retrievalCount} retrievals)`)
|
||||
}
|
||||
|
||||
function addDataPoints(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
emotionalData: DataPoint[],
|
||||
baselineData: DataPoint[],
|
||||
xScale: d3.ScaleLinear<number, number>,
|
||||
yScale: d3.ScaleLinear<number, number>,
|
||||
): void {
|
||||
// Add key data points for emotional line
|
||||
svg.selectAll('.point-emotional')
|
||||
.data(emotionalData.filter((d, i) => i === 0 || i === 1 || i % 5 === 0))
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'point-emotional')
|
||||
.attr('cx', d => xScale(d.x))
|
||||
.attr('cy', d => yScale(d.y))
|
||||
.attr('r', (d, i) => i < 2 ? 6 : 4)
|
||||
.attr('fill', '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)
|
||||
|
||||
const baselinePoint = baselineData.find(bd => bd.x === d.x)
|
||||
const baselineValue = baselinePoint ? Math.round(baselinePoint.y) : 'N/A'
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 1)
|
||||
.html(`
|
||||
<div class="font-semibold">${d.label}</div>
|
||||
<div>Total: ${Math.round(d.y)}</div>
|
||||
<div>Base: ${baselineValue}</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 : 4)
|
||||
|
||||
tooltip.value
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 0)
|
||||
})
|
||||
|
||||
// Add labels to important emotional points
|
||||
svg.selectAll('.emotional-point-label')
|
||||
.data(emotionalData.filter((d, i) => i === 0 || i === 1 || i === emotionalData.length - 1))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'emotional-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', 'rgba(59, 130, 246, 1)')
|
||||
.text(d => Math.round(d.y))
|
||||
}
|
||||
|
||||
function addTitleAndLegends(
|
||||
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||
memoryId: string,
|
||||
joyScore: number,
|
||||
aversionScore: number,
|
||||
retrievalCount: number,
|
||||
width: number,
|
||||
): void {
|
||||
// Add emotional scores label
|
||||
const joyPercent = Math.round(joyScore * 100)
|
||||
const aversionPercent = Math.round(aversionScore * 100)
|
||||
|
||||
// Add legend
|
||||
const legendData = [
|
||||
{ label: 'Emotional Memory', color: 'rgba(59, 130, 246, 0.8)' },
|
||||
{ label: 'Base Decay', color: 'rgba(156, 163, 175, 0.8)' },
|
||||
{ label: `Joy Component (${joyPercent}%)`, color: 'rgba(250, 204, 21, 0.8)' },
|
||||
{ label: `Aversion Component (${aversionPercent}%)`, color: 'rgba(220, 38, 38, 0.8)' },
|
||||
]
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', `translate(${width - 220}, 10)`)
|
||||
|
||||
const legendItems = legend.selectAll('.legend-item')
|
||||
.data(legendData)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'legend-item')
|
||||
.attr('transform', (d, i) => `translate(10, ${i * 20})`)
|
||||
|
||||
legendItems.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', 8)
|
||||
.attr('x2', 20)
|
||||
.attr('y2', 8)
|
||||
.attr('stroke', d => d.color)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', (d, i) => i === 0 ? '0' : i === 1 ? '5,3' : '3,2')
|
||||
|
||||
legendItems.append('text')
|
||||
.attr('x', 25)
|
||||
.attr('y', 12)
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(d => d.label)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Setup resize observer
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (props.memoryData.length && props.selectedMemoryId) {
|
||||
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-lg font-semibold">
|
||||
Emotional Memory Projection
|
||||
</h2>
|
||||
<!-- D3.js chart container -->
|
||||
<div ref="chartContainer" class="relative h-[800px] 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),
|
||||
:deep(.domain) {
|
||||
stroke: #dce0e3;
|
||||
}
|
||||
|
||||
.dark {
|
||||
:deep(.x-axis path),
|
||||
:deep(.y-axis path),
|
||||
:deep(.x-axis line),
|
||||
:deep(.y-axis line),
|
||||
:deep(.domain) {
|
||||
stroke: #374151;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<script setup lang="ts">
|
||||
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memory: EmotionalMemoryItem
|
||||
longTermThreshold: number
|
||||
muscleMemoryThreshold: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['retrieve'])
|
||||
|
||||
// Calculate age in days
|
||||
const ageInDays = computed(() => {
|
||||
return Math.round(props.memory.age_in_seconds / (24 * 60 * 60))
|
||||
})
|
||||
|
||||
// Calculate time since last retrieval
|
||||
const daysSinceRetrieved = computed(() => {
|
||||
return Math.round(props.memory.time_since_retrieval / (24 * 60 * 60))
|
||||
})
|
||||
|
||||
// Calculate memory status
|
||||
const memoryStatus = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
|
||||
return {
|
||||
type: 'muscle-memory',
|
||||
label: 'Muscle Memory',
|
||||
color: 'text-purple-600 dark:text-purple-400',
|
||||
}
|
||||
}
|
||||
else if (props.memory.retrieval_count >= props.longTermThreshold) {
|
||||
return {
|
||||
type: 'long-term',
|
||||
label: 'Long-term Memory',
|
||||
color: 'text-indigo-600 dark:text-indigo-400',
|
||||
}
|
||||
}
|
||||
else if (props.memory.retrieval_count > 0) {
|
||||
return {
|
||||
type: 'working',
|
||||
label: 'Working Memory',
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
type: 'short-term',
|
||||
label: 'Short-term Memory',
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Joy and aversion levels as percentage
|
||||
const joyPercentage = computed(() => {
|
||||
return Math.round(props.memory.joy_score * 100)
|
||||
})
|
||||
|
||||
const aversionPercentage = computed(() => {
|
||||
return Math.round(props.memory.aversion_score * 100)
|
||||
})
|
||||
|
||||
// Progress percentage for memory strength
|
||||
const strengthPercentage = computed(() => {
|
||||
return Math.min(100, Math.round((props.memory.decayed_score / props.memory.score) * 100))
|
||||
})
|
||||
|
||||
// Progress towards long-term memory
|
||||
const ltmPercentage = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.longTermThreshold) {
|
||||
return 100
|
||||
}
|
||||
return Math.round((props.memory.retrieval_count / props.longTermThreshold) * 100)
|
||||
})
|
||||
|
||||
// Progress towards muscle memory
|
||||
const musclePercentage = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
|
||||
return 100
|
||||
}
|
||||
if (props.memory.retrieval_count < props.longTermThreshold) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(((props.memory.retrieval_count - props.longTermThreshold)
|
||||
/ (props.muscleMemoryThreshold - props.longTermThreshold)) * 100)
|
||||
})
|
||||
|
||||
// Emotional effect on memory score
|
||||
const emotionalMultiplier = computed(() => {
|
||||
// This is a simplified calculation - would match your SQL formula
|
||||
const joyEffect = props.memory.joy_score > 0 ? props.memory.joy_score : 0
|
||||
const aversionEffect = props.memory.aversion_score > 0 ? props.memory.aversion_score : 0
|
||||
const combined = 1 + joyEffect + aversionEffect
|
||||
return Math.round(combined * 100)
|
||||
})
|
||||
|
||||
function simulateRetrieval(emotionalResponse = null) {
|
||||
if (emotionalResponse === 'joy') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: 0.1, aversionModifier: -0.05 })
|
||||
}
|
||||
else if (emotionalResponse === 'aversion') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: -0.05, aversionModifier: 0.1 })
|
||||
}
|
||||
else if (emotionalResponse === 'neutral') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: 0, aversionModifier: 0 })
|
||||
}
|
||||
else if (emotionalResponse === 'strong-joy') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: 0.2, aversionModifier: -0.1 })
|
||||
}
|
||||
else if (emotionalResponse === 'strong-aversion') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: -0.1, aversionModifier: 0.2 })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<h3 class="text-xl font-bold">
|
||||
{{ memory.id }}
|
||||
</h3>
|
||||
<span
|
||||
class="inline-flex items-center rounded-lg px-2 py-1 text-xs font-medium"
|
||||
:class="{
|
||||
'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300': memoryStatus.type === 'muscle-memory',
|
||||
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300': memoryStatus.type === 'long-term',
|
||||
'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300': memoryStatus.type === 'working',
|
||||
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300': memoryStatus.type === 'short-term',
|
||||
}"
|
||||
>
|
||||
{{ memoryStatus.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 mb-4 gap-3 text-sm font-mono">
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Base Score:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ Math.round(memory.score) }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Current Score:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ Math.round(memory.decayed_score) }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Emotional Effect:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ emotionalMultiplier }}%
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Retrieval Count:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ memory.retrieval_count }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Memory Age:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ ageInDays }} days
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Last Retrieved:
|
||||
</div>
|
||||
<div class="text-right font-medium">
|
||||
{{ daysSinceRetrieved > 0 ? `${daysSinceRetrieved} days ago` : 'Today' }}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Joy Score:
|
||||
</div>
|
||||
<div
|
||||
class="text-right font-medium"
|
||||
:class="joyPercentage > 50 ? 'text-yellow-500 dark:text-yellow-400' : ''"
|
||||
>
|
||||
{{ joyPercentage }}%
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Aversion Score:
|
||||
</div>
|
||||
<div
|
||||
class="text-right font-medium"
|
||||
:class="aversionPercentage > 50 ? 'text-red-500 dark:text-red-400' : ''"
|
||||
>
|
||||
{{ aversionPercentage }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emotional retrieval buttons -->
|
||||
<div class="mb-6">
|
||||
<h4 class="mb-2 text-sm text-neutral-500 font-medium dark:text-neutral-400">
|
||||
Simulate Retrieval with Emotion:
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-yellow-100 px-3 py-2 text-sm font-medium dark:bg-yellow-900/50 hover:bg-yellow-200 dark:hover:bg-yellow-900"
|
||||
@click="simulateRetrieval('strong-joy')"
|
||||
>
|
||||
Very Joyful
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-yellow-50 px-3 py-2 text-sm font-medium dark:bg-yellow-900/20 hover:bg-yellow-100 dark:hover:bg-yellow-900/40"
|
||||
@click="simulateRetrieval('joy')"
|
||||
>
|
||||
Mild Joy
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-neutral-100 px-3 py-2 text-sm font-medium dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
@click="simulateRetrieval('neutral')"
|
||||
>
|
||||
Neutral
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-50 px-3 py-2 text-sm font-medium dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40"
|
||||
@click="simulateRetrieval('aversion')"
|
||||
>
|
||||
Mild Aversion
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-100 px-3 py-2 text-sm font-medium dark:bg-red-900/50 hover:bg-red-200 dark:hover:bg-red-900"
|
||||
@click="simulateRetrieval('strong-aversion')"
|
||||
>
|
||||
Strong Aversion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visual representation of memory state -->
|
||||
<div>
|
||||
<!-- Memory Strength Progress -->
|
||||
<div class="mt-4 font-mono">
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Current 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="{
|
||||
'bg-purple-400': memoryStatus.type === 'muscle-memory',
|
||||
'bg-indigo-500': memoryStatus.type === 'long-term',
|
||||
'bg-blue-500': memoryStatus.type === 'working',
|
||||
'bg-red-500': memoryStatus.type === 'short-term',
|
||||
}"
|
||||
:style="`width: ${strengthPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-xs">
|
||||
<span>0%</span>
|
||||
<span>{{ Math.round(memory.score / 2) }}</span>
|
||||
<span>{{ Math.round(memory.score) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Formation Progress -->
|
||||
<div 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-indigo-400 transition-all duration-500"
|
||||
:style="`width: ${ltmPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between text-xs">
|
||||
<span>Working</span>
|
||||
<span v-if="memory.retrieval_count < longTermThreshold" class="text-sm font-medium">
|
||||
{{ memory.retrieval_count }}/{{ longTermThreshold }} retrievals
|
||||
</span>
|
||||
<span v-else class="text-sm text-indigo-600 font-medium dark:text-indigo-400">
|
||||
LTM Stabled
|
||||
</span>
|
||||
<span>Long-term</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Muscle Memory Progress -->
|
||||
<div class="mt-4 font-mono">
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Muscle 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: ${musclePercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between text-xs">
|
||||
<span>Conscious</span>
|
||||
<span v-if="memory.retrieval_count < muscleMemoryThreshold" class="text-sm font-medium">
|
||||
{{ memory.retrieval_count }}/{{ muscleMemoryThreshold }} retrievals
|
||||
</span>
|
||||
<span v-else class="text-sm text-purple-600 font-medium dark:text-purple-400">
|
||||
MM formed
|
||||
</span>
|
||||
<span>Automatic</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emotional Components -->
|
||||
<div class="grid grid-cols-2 mt-4 gap-4">
|
||||
<div>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Joy Factor:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full bg-yellow-400 transition-all duration-500"
|
||||
:style="`width: ${joyPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Aversion Factor:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full bg-red-400 transition-all duration-500"
|
||||
:style="`width: ${aversionPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memory: any
|
||||
longTermMemoryThreshold: number
|
||||
muscleMemoryThreshold: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['retrieve'])
|
||||
|
||||
// Joy and aversion values
|
||||
const joyLevel = computed(() => {
|
||||
return props.memory.joy_score || 0
|
||||
})
|
||||
|
||||
const aversionLevel = computed(() => {
|
||||
return props.memory.aversion_score || 0
|
||||
})
|
||||
|
||||
// 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.memory.retrieval_count >= props.muscleMemoryThreshold) {
|
||||
return {
|
||||
type: 'muscle-memory',
|
||||
label: 'Muscle Memory',
|
||||
color: 'text-purple-600 dark:text-purple-400',
|
||||
}
|
||||
}
|
||||
else if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
|
||||
return {
|
||||
type: 'long-term',
|
||||
label: 'Long-term',
|
||||
color: 'text-indigo-600 dark:text-indigo-400',
|
||||
}
|
||||
}
|
||||
else if (props.memory.retrieval_count > 0) {
|
||||
return {
|
||||
type: 'working',
|
||||
label: 'Working',
|
||||
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 towards long-term memory
|
||||
const ltmPercentage = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
|
||||
return 100
|
||||
}
|
||||
return Math.round((props.memory.retrieval_count / props.longTermMemoryThreshold) * 100)
|
||||
})
|
||||
|
||||
// Progress towards muscle memory
|
||||
const musclePercentage = computed(() => {
|
||||
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
|
||||
return 100
|
||||
}
|
||||
return Math.round((props.memory.retrieval_count / props.muscleMemoryThreshold) * 100)
|
||||
})
|
||||
|
||||
function simulateRetrieval(emotionalResponse = null) {
|
||||
if (emotionalResponse === 'joy') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: 0.1, aversionModifier: -0.05 })
|
||||
}
|
||||
else if (emotionalResponse === 'aversion') {
|
||||
emit('retrieve', props.memory.id, { joyModifier: -0.05, aversionModifier: 0.1 })
|
||||
}
|
||||
else {
|
||||
emit('retrieve', props.memory.id, { joyModifier: 0, aversionModifier: 0 })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<h3 class="font-bold">
|
||||
{{ memory.id }}
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<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()"
|
||||
>
|
||||
Neutral Retrieval
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 mb-4 gap-3" text-sm font-mono>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Memory 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>
|
||||
Base 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>
|
||||
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 class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Joy Score:
|
||||
</div>
|
||||
<div class="font-medium" text-right :class="joyLevel > 0.5 ? 'text-yellow-500' : ''">
|
||||
{{ (joyLevel * 100).toFixed(0) }}%
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Aversion Score:
|
||||
</div>
|
||||
<div class="font-medium" text-right :class="aversionLevel > 0.5 ? 'text-red-500' : ''">
|
||||
{{ (aversionLevel * 100).toFixed(0) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emotional response buttons -->
|
||||
<div class="grid grid-cols-2 mb-4 gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-yellow-100 px-3 py-2 text-sm font-medium dark:bg-yellow-900 hover:bg-yellow-200 dark:hover:bg-yellow-800"
|
||||
@click="simulateRetrieval('joy')"
|
||||
>
|
||||
Joyful Retrieval (+0.1)
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-100 px-3 py-2 text-sm font-medium dark:bg-red-900 hover:bg-red-200 dark:hover:bg-red-800"
|
||||
@click="simulateRetrieval('aversion')"
|
||||
>
|
||||
Aversive Retrieval (+0.1)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Visual representation of current state -->
|
||||
<div>
|
||||
<!-- Memory Strength Progress -->
|
||||
<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="memoryStatus.type === 'muscle-memory' ? 'bg-purple-400' : memoryStatus.type === 'long-term' ? 'bg-indigo-500' : '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>
|
||||
|
||||
<!-- Long-term Memory Progress -->
|
||||
<div 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-indigo-400 transition-all duration-500"
|
||||
:style="`width: ${ltmPercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between text-xs">
|
||||
<span>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-indigo-600 font-medium dark:text-indigo-400">
|
||||
LTM formed
|
||||
</span>
|
||||
<span>Long-term</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Muscle Memory Progress -->
|
||||
<div class="mt-4" font-mono>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Muscle 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: ${musclePercentage}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between text-xs">
|
||||
<span>Conscious</span>
|
||||
<span v-if="memory.retrieval_count < muscleMemoryThreshold" class="text-sm font-medium">
|
||||
{{ memory.retrieval_count }}/{{ muscleMemoryThreshold }} retrievals
|
||||
</span>
|
||||
<span v-else class="text-sm text-purple-600 font-medium dark:text-purple-400">
|
||||
MM formed
|
||||
</span>
|
||||
<span>Automatic</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emotional Components -->
|
||||
<div class="grid grid-cols-2 mt-4 gap-4">
|
||||
<div>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Joy Factor:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full bg-yellow-400 transition-all duration-500"
|
||||
:style="`width: ${joyLevel * 100}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
|
||||
Aversion Factor:
|
||||
</div>
|
||||
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
|
||||
<div
|
||||
class="h-full max-w-full bg-red-400 transition-all duration-500"
|
||||
:style="`width: ${aversionLevel * 100}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Range from '../Range.vue'
|
||||
|
||||
const joyBoostFactor = defineModel<number>('joyBoostFactor', { default: 1.5 })
|
||||
const joyDecaySteepness = defineModel<number>('joyDecaySteepness', { default: 3.0 })
|
||||
const aversionSpikeFactor = defineModel<number>('aversionSpikeFactor', { default: 2.0 })
|
||||
const aversionStability = defineModel<number>('aversionStability', { default: 1.2 })
|
||||
const randomRecallProbability = defineModel<number>('randomRecallProbability', { default: 0.05 })
|
||||
const flashbackIntensity = defineModel<number>('flashbackIntensity', { default: 2.0 })
|
||||
|
||||
// Computed percent values for display
|
||||
const randomRecallPercent = computed(() => (randomRecallProbability.value * 100).toFixed(0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="my-4 rounded-lg bg-neutral-100 dark:bg-neutral-800/50">
|
||||
<h2 class="mb-2 text-lg font-semibold">
|
||||
Emotional Memory Parameters
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- Joy/Euphoria Settings -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Joy Boost Factor</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="joyBoostFactor"
|
||||
:min="0.1"
|
||||
:max="3.0"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ joyBoostFactor.toFixed(1) }}x</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How much joy increases memory strength
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Joy Decay Steepness</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="joyDecaySteepness"
|
||||
:min="0.5"
|
||||
:max="5.0"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ joyDecaySteepness.toFixed(1) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How quickly joy effect fades (higher = faster)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aversion/Trauma Settings -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Aversion Spike Factor</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="aversionSpikeFactor"
|
||||
:min="0.1"
|
||||
:max="5.0"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ aversionSpikeFactor.toFixed(1) }}x</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How strongly aversive memories spike in recall
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Aversion Stability</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="aversionStability"
|
||||
:min="1.0"
|
||||
:max="1.5"
|
||||
:step="0.05"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ aversionStability.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How persistent aversive memories become (PTSD-like)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Random Recall/Flashback -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Random Recall Probability</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="randomRecallProbability"
|
||||
:min="0"
|
||||
:max="0.3"
|
||||
:step="0.01"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ randomRecallPercent }}%</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
Chance of random memory flashbacks
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-medium">Flashback Intensity</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="flashbackIntensity"
|
||||
:min="1.0"
|
||||
:max="5.0"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-16 text-right font-mono">{{ flashbackIntensity.toFixed(1) }}x</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
How strong random memory flashbacks can be
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
<script setup lang="ts">
|
||||
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
|
||||
|
||||
import * as d3 from 'd3'
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memoryData: EmotionalMemoryItem[]
|
||||
simulatedTimeOffset: number
|
||||
timeRange: number // days to display in the heatmap
|
||||
}>()
|
||||
|
||||
interface HeatmapCell {
|
||||
id: string
|
||||
day: number
|
||||
value: number
|
||||
isRetrieval: boolean
|
||||
isRandom: boolean
|
||||
joyScore: number
|
||||
aversionScore: number
|
||||
}
|
||||
|
||||
const heatmapContainer = ref<HTMLDivElement>()
|
||||
const tooltip = ref<d3.Selection<HTMLDivElement, unknown, null, undefined>>()
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
|
||||
// Add mode toggle
|
||||
const viewMode = ref<'patterns' | 'counts'>('patterns')
|
||||
|
||||
watchEffect(() => {
|
||||
if (heatmapContainer.value && props.memoryData.length > 0) {
|
||||
viewMode.value === 'patterns' ? renderHeatmap() : renderCountHeatmap()
|
||||
}
|
||||
})
|
||||
|
||||
function renderHeatmap() {
|
||||
// Clear existing chart
|
||||
d3.select(heatmapContainer.value).selectAll('*').remove()
|
||||
|
||||
// Get container dimensions
|
||||
const containerRect = heatmapContainer.value.getBoundingClientRect()
|
||||
const containerWidth = containerRect.width
|
||||
const containerHeight = 300 // Fixed height for the heatmap
|
||||
|
||||
// Set chart margins
|
||||
const margin = { top: 30, right: 20, bottom: 60, left: 120 }
|
||||
const width = containerWidth - margin.left - margin.right
|
||||
const height = containerHeight - margin.top - margin.bottom
|
||||
|
||||
// Prepare data for heatmap
|
||||
const heatmapData: HeatmapCell[] = []
|
||||
|
||||
// Generate heatmap data from memory items
|
||||
for (const memory of props.memoryData) {
|
||||
// const ageInDays = Math.floor(memory.age_in_seconds / (24 * 60 * 60))
|
||||
const retrievalDaysAgo = Math.floor(memory.time_since_retrieval / (24 * 60 * 60))
|
||||
|
||||
// Only consider memories retrieved within the time range
|
||||
if (retrievalDaysAgo <= props.timeRange) {
|
||||
// For each memory, add a heatmap cell for the retrieval day
|
||||
heatmapData.push({
|
||||
id: memory.id,
|
||||
day: props.timeRange - retrievalDaysAgo,
|
||||
value: 1, // Strength of retrieval
|
||||
isRetrieval: true,
|
||||
isRandom: Math.random() < 0.2, // Simulate random recall events (20% chance)
|
||||
joyScore: memory.joy_score,
|
||||
aversionScore: memory.aversion_score,
|
||||
})
|
||||
|
||||
// Add additional cells for fade trail of past retrievals
|
||||
const fadeLength = 5 // Days of fade trail
|
||||
for (let i = 1; i <= fadeLength; i++) {
|
||||
if (props.timeRange - retrievalDaysAgo - i >= 0) {
|
||||
heatmapData.push({
|
||||
id: memory.id,
|
||||
day: props.timeRange - retrievalDaysAgo - i,
|
||||
value: 0.2 * (fadeLength - i + 1) / fadeLength, // Decreasing intensity
|
||||
isRetrieval: false,
|
||||
isRandom: false,
|
||||
joyScore: memory.joy_score * 0.5, // Reduced emotional impact
|
||||
aversionScore: memory.aversion_score * 0.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create tooltip if it doesn't exist
|
||||
if (!tooltip.value) {
|
||||
tooltip.value = d3.select(heatmapContainer.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(heatmapContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
|
||||
// Extract unique memory IDs
|
||||
const memoryIds = Array.from(new Set(props.memoryData.map(m => m.id)))
|
||||
|
||||
// Set up scales
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, props.timeRange])
|
||||
.range([0, width])
|
||||
|
||||
const yScale = d3.scaleBand()
|
||||
.domain(memoryIds)
|
||||
.range([0, height])
|
||||
.padding(0.1)
|
||||
|
||||
const colorScale = d3.scaleSequential()
|
||||
.domain([0, 1])
|
||||
.interpolator(d3.interpolateBlues)
|
||||
|
||||
// Create x-axis (time)
|
||||
const xAxis = d3.axisBottom(xScale)
|
||||
.ticks(Math.min(props.timeRange, 10))
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(xAxis)
|
||||
.selectAll('text')
|
||||
.attr('transform', 'rotate(-45)')
|
||||
.style('text-anchor', 'end')
|
||||
|
||||
// Create y-axis (memory IDs)
|
||||
const yAxis = d3.axisLeft(yScale)
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis)
|
||||
|
||||
// Create heatmap cells
|
||||
svg.selectAll('.heatmap-cell')
|
||||
.data(heatmapData)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'heatmap-cell')
|
||||
.attr('x', d => xScale(d.day))
|
||||
.attr('y', d => yScale(d.id))
|
||||
.attr('width', _d => Math.max(2, width / props.timeRange)) // Ensure cells are visible
|
||||
.attr('height', yScale.bandwidth())
|
||||
.attr('fill', (d) => {
|
||||
if (d.isRetrieval) {
|
||||
// Create color based on emotional components
|
||||
if (d.joyScore > 0.5)
|
||||
return d3.interpolateYlOrRd(d.joyScore)
|
||||
if (d.aversionScore > 0.5)
|
||||
return d3.interpolatePurples(d.aversionScore)
|
||||
return colorScale(d.value)
|
||||
}
|
||||
else {
|
||||
// Fade trails
|
||||
return colorScale(d.value)
|
||||
}
|
||||
})
|
||||
.attr('stroke', d => d.isRandom ? 'rgba(255, 0, 0, 0.5)' : 'none')
|
||||
.attr('stroke-width', 1)
|
||||
.on('mouseover', function (event, d) {
|
||||
d3.select(this)
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2)
|
||||
|
||||
let tooltipContent = `
|
||||
<div class="font-semibold">${d.id}</div>
|
||||
<div>${d.isRetrieval ? 'Retrieval' : 'Echo'}</div>
|
||||
<div>${props.timeRange - d.day} days ago</div>
|
||||
`
|
||||
|
||||
if (d.isRetrieval) {
|
||||
tooltipContent += `
|
||||
<div>Joy: ${Math.round(d.joyScore * 100)}%</div>
|
||||
<div>Aversion: ${Math.round(d.aversionScore * 100)}%</div>
|
||||
${d.isRandom ? '<div class="text-red-500">Random Recall</div>' : ''}
|
||||
`
|
||||
}
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 1)
|
||||
.html(tooltipContent)
|
||||
.style('left', `${event.offsetX + 10}px`)
|
||||
.style('top', `${event.offsetY - 15}px`)
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
d3.select(this)
|
||||
// @ts-expect-error - d is of type HeatmapCell
|
||||
.attr('stroke', d => d.isRandom ? 'rgba(255, 0, 0, 0.5)' : 'none')
|
||||
// @ts-expect-error - d is of type HeatmapCell
|
||||
.attr('stroke-width', d => d.isRandom ? 1 : 0)
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 0)
|
||||
})
|
||||
|
||||
// Add legend
|
||||
const legend = svg.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', `translate(${width - 120}, -30)`)
|
||||
|
||||
// Legend items
|
||||
const legendItems = [
|
||||
{ label: 'Retrieval', color: colorScale(1) },
|
||||
{ label: 'Joy', color: d3.interpolateYlOrRd(0.8) },
|
||||
{ label: 'Aversion', color: d3.interpolatePurples(0.8) },
|
||||
{ label: 'Random', color: 'rgba(70, 130, 180, 0.8)', stroke: 'rgba(255, 0, 0, 0.5)' },
|
||||
]
|
||||
|
||||
legendItems.forEach((item, i) => {
|
||||
const g = legend.append('g')
|
||||
.attr('transform', `translate(${i * 70}, 0)`)
|
||||
|
||||
g.append('rect')
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', item.color)
|
||||
.attr('stroke', item.stroke || 'none')
|
||||
.attr('stroke-width', item.stroke ? 1 : 0)
|
||||
|
||||
g.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', 12)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(item.label)
|
||||
})
|
||||
}
|
||||
|
||||
// New function to render the count heatmap
|
||||
function renderCountHeatmap() {
|
||||
// Clear existing chart
|
||||
d3.select(heatmapContainer.value).selectAll('*').remove()
|
||||
|
||||
// Get container dimensions
|
||||
const containerRect = heatmapContainer.value.getBoundingClientRect()
|
||||
const containerWidth = containerRect.width
|
||||
const containerHeight = 300 // Fixed height for the heatmap
|
||||
|
||||
// Set chart margins
|
||||
const margin = { top: 30, right: 20, bottom: 60, left: 120 }
|
||||
const width = containerWidth - margin.left - margin.right
|
||||
const height = containerHeight - margin.top - margin.bottom
|
||||
|
||||
// Create tooltip if it doesn't exist
|
||||
if (!tooltip.value) {
|
||||
tooltip.value = d3.select(heatmapContainer.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(heatmapContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
|
||||
// Extract unique memory IDs
|
||||
const memoryIds = Array.from(new Set(props.memoryData.map(m => m.id)))
|
||||
|
||||
// Get retrieval count data
|
||||
const countData = memoryIds.map((id) => {
|
||||
const memory = props.memoryData.find(m => m.id === id)
|
||||
return {
|
||||
id,
|
||||
count: memory ? memory.retrieval_count : 0,
|
||||
joyScore: memory ? memory.joy_score : 0,
|
||||
aversionScore: memory ? memory.aversion_score : 0,
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count) // Sort by count (highest first)
|
||||
|
||||
// Get sorted IDs for the y-axis
|
||||
const sortedIds = countData.map(d => d.id)
|
||||
|
||||
// Set up scales
|
||||
const yScale = d3.scaleBand()
|
||||
.domain(sortedIds)
|
||||
.range([0, height])
|
||||
.padding(0.1)
|
||||
|
||||
// Find max count for color scale
|
||||
const maxCount = d3.max(countData, d => d.count) || 1
|
||||
|
||||
// Create color scale for counts
|
||||
const colorScale = d3.scaleSequential()
|
||||
.domain([0, maxCount])
|
||||
.interpolator(d3.interpolateReds)
|
||||
|
||||
// Create bar width scale
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, maxCount])
|
||||
.range([0, width - 50]) // Leave space for count text
|
||||
|
||||
// Create y-axis (memory IDs)
|
||||
const yAxis = d3.axisLeft(yScale)
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis)
|
||||
|
||||
// Create count bars
|
||||
const bars = svg.selectAll('.count-bar')
|
||||
.data(countData)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'count-bar')
|
||||
.attr('transform', d => `translate(0, ${yScale(d.id)})`)
|
||||
|
||||
// Add bars
|
||||
bars.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', d => xScale(d.count))
|
||||
.attr('height', yScale.bandwidth())
|
||||
.attr('fill', (d) => {
|
||||
// Color based on emotional components and count
|
||||
if (d.joyScore > 0.5 && d.count > 0)
|
||||
return d3.interpolateYlOrRd(Math.min(1, d.joyScore * (d.count / maxCount)))
|
||||
if (d.aversionScore > 0.5 && d.count > 0)
|
||||
return d3.interpolatePurples(Math.min(1, d.aversionScore * (d.count / maxCount)))
|
||||
return colorScale(d.count)
|
||||
})
|
||||
.attr('stroke', 'rgba(255, 255, 255, 0.3)')
|
||||
.attr('stroke-width', 1)
|
||||
.on('mouseover', function (event, d) {
|
||||
d3.select(this)
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2)
|
||||
|
||||
const tooltipContent = `
|
||||
<div class="font-semibold">${d.id}</div>
|
||||
<div>Retrievals: ${d.count}</div>
|
||||
<div>Joy: ${Math.round(d.joyScore * 100)}%</div>
|
||||
<div>Aversion: ${Math.round(d.aversionScore * 100)}%</div>
|
||||
`
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 1)
|
||||
.html(tooltipContent)
|
||||
.style('left', `${event.offsetX + 10}px`)
|
||||
.style('top', `${event.offsetY - 15}px`)
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
d3.select(this)
|
||||
.attr('stroke', 'rgba(255, 255, 255, 0.3)')
|
||||
.attr('stroke-width', 1)
|
||||
|
||||
tooltip.value
|
||||
.style('opacity', 0)
|
||||
})
|
||||
|
||||
// Add count labels
|
||||
bars.append('text')
|
||||
.attr('x', d => xScale(d.count) + 5)
|
||||
.attr('y', yScale.bandwidth() / 2 + 5)
|
||||
.attr('fill', 'currentColor')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-weight', 'bold')
|
||||
.text(d => d.count)
|
||||
|
||||
// Add title
|
||||
svg.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -10)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '14px')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('fill', 'currentColor')
|
||||
.text('Memory Retrieval Counts')
|
||||
|
||||
// Add legend
|
||||
const legend = svg.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', `translate(${width - 120}, -30)`)
|
||||
|
||||
// Legend items
|
||||
const legendItems = [
|
||||
{ label: 'Low', color: colorScale(maxCount * 0.2) },
|
||||
{ label: 'Medium', color: colorScale(maxCount * 0.5) },
|
||||
{ label: 'High', color: colorScale(maxCount * 0.8) },
|
||||
{ label: 'Joy Impact', color: d3.interpolateYlOrRd(0.8) },
|
||||
{ label: 'Aversion', color: d3.interpolatePurples(0.8) },
|
||||
]
|
||||
|
||||
legendItems.forEach((item, i) => {
|
||||
const g = legend.append('g')
|
||||
.attr('transform', `translate(${(i % 3) * 70}, ${Math.floor(i / 3) * 20})`)
|
||||
|
||||
g.append('rect')
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', item.color)
|
||||
|
||||
g.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', 12)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', 'currentColor')
|
||||
.text(item.label)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Setup resize observer
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (props.memoryData.length > 0) {
|
||||
viewMode.value === 'patterns' ? renderHeatmap() : renderCountHeatmap()
|
||||
}
|
||||
})
|
||||
|
||||
if (heatmapContainer.value) {
|
||||
resizeObserver.observe(heatmapContainer.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Memory Retrieval Analysis
|
||||
</h2>
|
||||
<div class="flex rounded-lg bg-neutral-100 dark:bg-neutral-700">
|
||||
<button
|
||||
class="px-3 py-1 text-sm font-medium transition-colors"
|
||||
:class="viewMode === 'patterns' ? 'bg-blue-500 text-white rounded-lg' : 'text-neutral-600 dark:text-neutral-300'"
|
||||
@click="viewMode = 'patterns'"
|
||||
>
|
||||
Patterns
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 text-sm font-medium transition-colors"
|
||||
:class="viewMode === 'counts' ? 'bg-blue-500 text-white rounded-lg' : 'text-neutral-600 dark:text-neutral-300'"
|
||||
@click="viewMode = 'counts'"
|
||||
>
|
||||
Counts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="heatmapContainer" class="relative h-[300px] w-full">
|
||||
<!-- D3.js will render the heatmap here -->
|
||||
</div>
|
||||
<div v-if="viewMode === 'patterns'" class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Heatmap shows when memories were retrieved, with color intensity showing emotional impact.
|
||||
Red outlines indicate possible random "flashback" retrievals.
|
||||
</div>
|
||||
<div v-else class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Bar chart shows total retrieval counts for each memory, sorted by frequency.
|
||||
Color intensity and hue indicate emotional impact.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.x-axis path),
|
||||
:deep(.y-axis path),
|
||||
:deep(.x-axis line),
|
||||
:deep(.y-axis line),
|
||||
:deep(.domain) {
|
||||
stroke: #dce0e3;
|
||||
}
|
||||
|
||||
.dark {
|
||||
:deep(.x-axis path),
|
||||
:deep(.y-axis path),
|
||||
:deep(.x-axis line),
|
||||
:deep(.y-axis line),
|
||||
:deep(.domain) {
|
||||
stroke: #374151;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
@@ -20,20 +19,19 @@ const props = withDefaults(defineProps<{
|
||||
trackValueColor: 'red',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
const modelValue = defineModel<number>('modelValue', { required: true })
|
||||
|
||||
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()
|
||||
const sliderValue = computed({
|
||||
get: () => modelValue.value * 10000,
|
||||
set: (value: number) => {
|
||||
modelValue.value = value / 10000
|
||||
updateTrackColor()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -113,3 +113,150 @@ export async function simulateRetrieval(db, storyId, simulatedTime) {
|
||||
WHERE id = '${storyId}'
|
||||
`)
|
||||
}
|
||||
|
||||
export async function generateEmotionalDecayQuery(db, {
|
||||
simulatedTimeOffset,
|
||||
decayRate,
|
||||
timeUnitInSeconds,
|
||||
longTermMemoryEnabled,
|
||||
longTermMemoryThreshold,
|
||||
longTermMemoryStability,
|
||||
retrievalBoost,
|
||||
retrievalDecaySlowdown,
|
||||
joyBoostFactor,
|
||||
joyDecaySteepness,
|
||||
aversionSpikeFactor,
|
||||
aversionStability,
|
||||
randomRecallProbability,
|
||||
flashbackIntensity,
|
||||
}) {
|
||||
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)`
|
||||
: ''
|
||||
|
||||
// New emotional memory components
|
||||
const emotionalComponents = `
|
||||
-- Random recall probability (flashback effect)
|
||||
(CASE WHEN RANDOM() < ${randomRecallProbability} THEN ${flashbackIntensity} ELSE 1 END) *
|
||||
|
||||
-- Joy/euphoria boost with steep decay
|
||||
(1 + (joy_score * ${joyBoostFactor} * EXP(-${joyDecaySteepness} *
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds})))) *
|
||||
|
||||
-- Aversion spike for traumatic memories
|
||||
(1 + (aversion_score * ${aversionSpikeFactor} *
|
||||
POWER(${aversionStability}, CEIL((retrieval_count / 5)))))
|
||||
`
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
score,
|
||||
updated_at,
|
||||
last_retrieved_at,
|
||||
retrieval_count,
|
||||
joy_score,
|
||||
aversion_score,
|
||||
CAST(updated_at AS VARCHAR) as updated_at_str,
|
||||
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str,
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at))) as age_in_seconds,
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at))) as time_since_retrieval,
|
||||
${ltmFactorClause}
|
||||
score *
|
||||
exp(-${decayRate} * (EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at)))/(${timeUnitInSeconds}) ${ltmDecayModifier}) *
|
||||
(1 + (retrieval_count * ${retrievalBoost} *
|
||||
exp(-${decayRate * retrievalDecaySlowdown} *
|
||||
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds})))) *
|
||||
${emotionalComponents} as decayed_score
|
||||
FROM emotional_memories_test_table
|
||||
ORDER BY decayed_score DESC
|
||||
`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export async function createEmotionalSchema(db) {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS emotional_memories_test_table (
|
||||
id VARCHAR,
|
||||
score DOUBLE,
|
||||
updated_at TIMESTAMP,
|
||||
last_retrieved_at TIMESTAMP,
|
||||
retrieval_count INTEGER DEFAULT 0,
|
||||
joy_score DOUBLE DEFAULT 0,
|
||||
aversion_score DOUBLE DEFAULT 0
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function loadEmotionalSampleData(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
|
||||
|
||||
// Generate emotional scores
|
||||
const joyScore = Math.random() * (i % 3 === 0 ? 0.8 : 0.3)
|
||||
const aversionScore = Math.random() * (i % 4 === 0 ? 0.7 : 0.2)
|
||||
|
||||
sampleData.push({
|
||||
id: `memory-${i}`,
|
||||
score,
|
||||
updated_at: lastUpdate.toISOString(),
|
||||
last_retrieved_at: lastRetrievedAt.toISOString(),
|
||||
retrieval_count: retrievalCount,
|
||||
joy_score: joyScore.toFixed(2),
|
||||
aversion_score: aversionScore.toFixed(2),
|
||||
})
|
||||
}
|
||||
|
||||
for (const item of sampleData) {
|
||||
await db.execute(`
|
||||
INSERT INTO emotional_memories_test_table
|
||||
(id, score, updated_at, last_retrieved_at, retrieval_count, joy_score, aversion_score)
|
||||
VALUES
|
||||
('${item.id}', ${item.score}, '${item.updated_at}', '${item.last_retrieved_at}',
|
||||
${item.retrieval_count}, ${item.joy_score}, ${item.aversion_score})
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function simulateEmotionalRetrieval(db, memoryId, simulatedTime, { joyModifier = 0, aversionModifier = 0 }) {
|
||||
const simulatedTimestamp = simulatedTime.toISOString()
|
||||
|
||||
await db.execute(`
|
||||
UPDATE emotional_memories_test_table
|
||||
SET last_retrieved_at = '${simulatedTimestamp}',
|
||||
retrieval_count = retrieval_count + 1,
|
||||
joy_score = CASE
|
||||
WHEN joy_score + ${joyModifier} < 0 THEN 0
|
||||
WHEN joy_score + ${joyModifier} > 1 THEN 1
|
||||
ELSE joy_score + ${joyModifier}
|
||||
END,
|
||||
aversion_score = CASE
|
||||
WHEN aversion_score + ${aversionModifier} < 0 THEN 0
|
||||
WHEN aversion_score + ${aversionModifier} > 1 THEN 1
|
||||
ELSE aversion_score + ${aversionModifier}
|
||||
END
|
||||
WHERE id = '${memoryId}'
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -273,6 +273,15 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</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"
|
||||
/>
|
||||
|
||||
<!-- Interactive Chart Section -->
|
||||
<div class="grid grid-cols-1 mb-4 gap-4 lg:grid-cols-3">
|
||||
<!-- Chart -->
|
||||
@@ -299,15 +308,6 @@ onUnmounted(() => {
|
||||
/>
|
||||
</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"
|
||||
|
||||
@@ -1,9 +1,614 @@
|
||||
<script setup lang="ts">
|
||||
import type { EmotionalMemoryItem } from '../types/memory/emotional-memory'
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
// Import the components
|
||||
import EmotionalMemoryChart from '../components/Memory/EmotionalMemoryChart.vue'
|
||||
import EmotionalMemoryDetail from '../components/Memory/EmotionalMemoryDetail.vue'
|
||||
import EmotionalSettings from '../components/Memory/EmotionalSettings.vue'
|
||||
import MemoryRetrievalHeatmap from '../components/Memory/MemoryRetrievalHeatmap.vue'
|
||||
import Range from '../components/Range.vue'
|
||||
import {
|
||||
connectToDatabase,
|
||||
createEmotionalSchema,
|
||||
generateEmotionalDecayQuery as generateEmotionalQuery,
|
||||
loadEmotionalSampleData,
|
||||
simulateEmotionalRetrieval,
|
||||
} from '../composables/memory/memory-decay-db'
|
||||
|
||||
// Database references
|
||||
const db = ref(null)
|
||||
const isMigrated = ref(false)
|
||||
|
||||
// Data and query results
|
||||
const processedResults = ref<EmotionalMemoryItem[]>([])
|
||||
const selectedMemoryId = ref(null)
|
||||
|
||||
// Memory model parameters
|
||||
const decayRate = ref(0.0990)
|
||||
const timeUnit = ref('days')
|
||||
const maxDaysToProject = ref(2000)
|
||||
|
||||
// Emotional memory parameters
|
||||
const joyBoostFactor = ref(1.5)
|
||||
const joyDecaySteepness = ref(3.0)
|
||||
const aversionSpikeFactor = ref(2.0)
|
||||
const aversionStability = ref(1.2)
|
||||
const randomRecallProbability = ref(0.05)
|
||||
const flashbackIntensity = ref(2.0)
|
||||
|
||||
// Time simulation parameters
|
||||
const timeMultiplier = ref(1) // Default: 1 day/second
|
||||
const isTimeAccelerated = ref(false)
|
||||
const simulatedTimeOffset = ref(0)
|
||||
const lastTickTime = ref(Date.now())
|
||||
|
||||
// Memory access thresholds
|
||||
const longTermThreshold = ref(5)
|
||||
const muscleMemoryThreshold = ref(40)
|
||||
|
||||
// UI controls
|
||||
const showAdvancedSettings = ref(false)
|
||||
|
||||
// Selected memory
|
||||
const selectedMemory = computed(() => {
|
||||
if (!selectedMemoryId.value || !processedResults.value.length)
|
||||
return null
|
||||
return processedResults.value.find(m => m.id === selectedMemoryId.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
|
||||
})
|
||||
|
||||
// Add a new ref for heatmap configuration
|
||||
const heatmapTimeRange = ref(30) // Show last 30 days by default
|
||||
|
||||
// Add this after the other refs
|
||||
const memoryHistory = ref(new Map())
|
||||
|
||||
// Maximum number of history points to track per memory
|
||||
const historyLength = 20
|
||||
|
||||
// Initialize database and load data
|
||||
async function initialize() {
|
||||
db.value = await connectToDatabase()
|
||||
await createEmotionalSchema(db.value)
|
||||
await loadEmotionalSampleData(db.value)
|
||||
isMigrated.value = true
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// Emotional decay query
|
||||
const emotionalDecayQuery = ref('')
|
||||
|
||||
// Watch for parameter changes
|
||||
watch([
|
||||
simulatedTimeOffset,
|
||||
decayRate,
|
||||
timeUnit,
|
||||
longTermThreshold,
|
||||
muscleMemoryThreshold,
|
||||
joyBoostFactor,
|
||||
joyDecaySteepness,
|
||||
aversionSpikeFactor,
|
||||
aversionStability,
|
||||
randomRecallProbability,
|
||||
flashbackIntensity,
|
||||
], async () => {
|
||||
const query = await generateEmotionalQuery(db.value, {
|
||||
simulatedTimeOffset: simulatedTimeOffset.value,
|
||||
decayRate: decayRate.value,
|
||||
timeUnitInSeconds: timeUnitInSeconds.value,
|
||||
longTermMemoryEnabled: true,
|
||||
longTermMemoryThreshold: longTermThreshold.value,
|
||||
longTermMemoryStability: 0.3,
|
||||
retrievalBoost: 0.25,
|
||||
retrievalDecaySlowdown: 0.8,
|
||||
joyBoostFactor: joyBoostFactor.value,
|
||||
joyDecaySteepness: joyDecaySteepness.value,
|
||||
aversionSpikeFactor: aversionSpikeFactor.value,
|
||||
aversionStability: aversionStability.value,
|
||||
randomRecallProbability: randomRecallProbability.value,
|
||||
flashbackIntensity: flashbackIntensity.value,
|
||||
})
|
||||
|
||||
emotionalDecayQuery.value = query
|
||||
})
|
||||
|
||||
// Run the decay query
|
||||
async function runDecayQuery() {
|
||||
if (!emotionalDecayQuery.value)
|
||||
return
|
||||
|
||||
const queryResults = await db.value?.execute(emotionalDecayQuery.value) || []
|
||||
|
||||
// Update memory history with new data points
|
||||
for (const memory of queryResults) {
|
||||
// Initialize history array if doesn't exist
|
||||
if (!memoryHistory.value.has(memory.id)) {
|
||||
memoryHistory.value.set(memory.id, [])
|
||||
}
|
||||
|
||||
const history = memoryHistory.value.get(memory.id)
|
||||
|
||||
// Add current state to history with timestamp
|
||||
history.push({
|
||||
timestamp: Date.now(),
|
||||
simulatedTime: simulatedTimeOffset.value,
|
||||
score: memory.decayed_score,
|
||||
joy: memory.joy_score,
|
||||
aversion: memory.aversion_score,
|
||||
retrievalCount: memory.retrieval_count,
|
||||
})
|
||||
|
||||
// Trim history to maintain fixed length
|
||||
if (history.length > historyLength) {
|
||||
history.shift()
|
||||
}
|
||||
}
|
||||
|
||||
processedResults.value = queryResults
|
||||
|
||||
// Initialize selectedMemoryId if not set or update if selection changed
|
||||
if (!selectedMemoryId.value && processedResults.value.length) {
|
||||
selectedMemoryId.value = processedResults.value[0].id
|
||||
}
|
||||
|
||||
generateChartData()
|
||||
}
|
||||
|
||||
// Generate chart data
|
||||
function generateChartData() {
|
||||
// Add calculated properties to the memory items for easier access
|
||||
processedResults.value = processedResults.value.map((item) => {
|
||||
const ageInDays = Math.round(item.age_in_seconds / (24 * 60 * 60))
|
||||
return {
|
||||
...item,
|
||||
age_in_days: ageInDays,
|
||||
joyComponent: item.joy_score * joyBoostFactor.value,
|
||||
aversionComponent: item.aversion_score * aversionSpikeFactor.value,
|
||||
effective_score: item.decayed_score,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle simulated retrieval with emotional response
|
||||
async function handleRetrieval(memoryId, { joyModifier = 0, aversionModifier = 0 } = {}) {
|
||||
await simulateEmotionalRetrieval(db.value, memoryId, new Date(Date.now() + simulatedTimeOffset.value * 1000), {
|
||||
joyModifier,
|
||||
aversionModifier,
|
||||
})
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// New function to process random retrievals
|
||||
async function processRandomRetrievals() {
|
||||
// Only run if database is initialized
|
||||
if (!db.value || !processedResults.value?.length)
|
||||
return
|
||||
|
||||
// For each memory, check if it should be randomly retrieved based on randomRecallProbability
|
||||
for (const memory of processedResults.value) {
|
||||
// Apply the same random check that's in the SQL
|
||||
if (Math.random() < randomRecallProbability.value) {
|
||||
// Determine if this should be a joy or aversion-based recall
|
||||
// Use the existing emotional components to guide this
|
||||
let joyModifier = 0
|
||||
let aversionModifier = 0
|
||||
|
||||
if (memory.joy_score > memory.aversion_score && memory.joy_score > 0.3) {
|
||||
joyModifier = 0.05
|
||||
}
|
||||
else if (memory.aversion_score > 0.3) {
|
||||
aversionModifier = 0.05
|
||||
}
|
||||
|
||||
// Call the regular retrieval function
|
||||
await simulateEmotionalRetrieval(
|
||||
db.value,
|
||||
memory.id,
|
||||
new Date(Date.now() + simulatedTimeOffset.value * 1000),
|
||||
{ joyModifier, aversionModifier },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the UI
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// Handle memory projection reset
|
||||
async function resetProjection() {
|
||||
await loadEmotionalSampleData(db.value) // true flag to reset data
|
||||
simulatedTimeOffset.value = 0
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// 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
|
||||
case 'year':
|
||||
secondsToAdd = 365 * 24 * 60 * 60
|
||||
break
|
||||
}
|
||||
|
||||
simulatedTimeOffset.value += amount * secondsToAdd
|
||||
await runDecayQuery()
|
||||
}
|
||||
|
||||
// Time acceleration handling
|
||||
function baseTickTime() {
|
||||
if (!isTimeAccelerated.value)
|
||||
return
|
||||
|
||||
const now = Date.now()
|
||||
const elapsedMs = now - lastTickTime.value
|
||||
simulatedTimeOffset.value += (elapsedMs / 1000) * timeMultiplier.value
|
||||
lastTickTime.value = now
|
||||
|
||||
runDecayQuery()
|
||||
|
||||
// Process random retrievals based on elapsed time
|
||||
// We'll check for random retrievals every few seconds of simulated time
|
||||
if (Math.random() < (elapsedMs / 5000) * timeMultiplier.value) {
|
||||
processRandomRetrievals()
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedTickTime = useDebounceFn(() => {
|
||||
baseTickTime()
|
||||
requestAnimationFrame(debouncedTickTime)
|
||||
}, 1000)
|
||||
|
||||
// Add a button to manually trigger random retrievals for testing
|
||||
async function triggerRandomRetrievals() {
|
||||
await processRandomRetrievals()
|
||||
}
|
||||
|
||||
// Watch time acceleration state
|
||||
watch(isTimeAccelerated, (newValue) => {
|
||||
if (newValue) {
|
||||
lastTickTime.value = Date.now()
|
||||
debouncedTickTime()
|
||||
}
|
||||
})
|
||||
|
||||
// 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>
|
||||
<h1>Memory Simulator</h1>
|
||||
<template v-if="!isMigrated">
|
||||
<!-- Loading state -->
|
||||
<div 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>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<header class="mb-4">
|
||||
<h1 class="mb-2 text-2xl font-bold">
|
||||
Memory Flashback & Retrieval Simulator
|
||||
</h1>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="max-w-2xl text-neutral-600 dark:text-neutral-300">
|
||||
Visualize how memories could be retrieved and impacted by emotional state
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Time Controls -->
|
||||
<div class="my-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
|
||||
<h2 class="mb-2 flex items-center text-lg font-semibold" gap-4>
|
||||
<div flex-1>
|
||||
Time Simulation
|
||||
</div>
|
||||
<div class="text-sm font-mono">
|
||||
{{ currentSimulatedTime.toLocaleString() }}
|
||||
</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="isTimeAccelerated = !isTimeAccelerated"
|
||||
>
|
||||
<div v-if="isTimeAccelerated" i-solar:pause-bold />
|
||||
<div v-else i-solar:play-bold />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-neutral-200 px-4 py-2 font-medium dark:bg-neutral-700"
|
||||
@click="resetProjection"
|
||||
>
|
||||
<div i-solar:restart-line-duotone />
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<!-- 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="handleTimeJump({ amount: 1, unit: 'day' })"
|
||||
>
|
||||
+1 Day
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="handleTimeJump({ amount: 1, unit: 'week' })"
|
||||
>
|
||||
+1 Week
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="handleTimeJump({ amount: 1, unit: 'month' })"
|
||||
>
|
||||
+1 Month
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="handleTimeJump({ amount: 1, unit: 'year' })"
|
||||
>
|
||||
+1 Year
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
|
||||
@click="handleTimeJump({ amount: 5, unit: 'year' })"
|
||||
>
|
||||
+5 Years
|
||||
</button>
|
||||
|
||||
<!-- New button to trigger random retrievals -->
|
||||
<button
|
||||
class="rounded-lg bg-purple-100 px-3 py-1 text-sm dark:bg-purple-900"
|
||||
title="Trigger random memory recalls based on probability settings"
|
||||
@click="triggerRandomRetrievals"
|
||||
>
|
||||
Random Recalls
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Speed controls -->
|
||||
<div class="mt-4">
|
||||
<h3 class="font-medium">
|
||||
Speed
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 mt-2 gap-2 md:grid-cols-6 sm:grid-cols-3">
|
||||
<button
|
||||
v-for="(speed, index) in [
|
||||
{ label: '1 second/s', value: 1 },
|
||||
{ label: '1 minute/s', value: 60 },
|
||||
{ label: '1 hour/s', value: 60 * 60 },
|
||||
{ label: '1 day/s', value: 60 * 60 * 24 },
|
||||
{ label: '1 week/s', value: 60 * 60 * 24 * 7 },
|
||||
{ label: '1 month/s', value: 60 * 60 * 24 * 30 },
|
||||
]"
|
||||
:key="index"
|
||||
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
||||
:class="timeMultiplier === speed.value ? 'bg-blue-200 dark:bg-blue-800' : 'bg-blue-100 dark:bg-blue-900'"
|
||||
@click="timeMultiplier = speed.value"
|
||||
>
|
||||
{{ speed.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MemoryRetrievalHeatmap
|
||||
:memory-data="processedResults"
|
||||
:simulated-time-offset="simulatedTimeOffset"
|
||||
:time-range="heatmapTimeRange"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Chart and Detail View -->
|
||||
<div class="grid grid-cols-1 mb-4 gap-4 lg:grid-cols-3">
|
||||
<!-- Chart -->
|
||||
<EmotionalMemoryChart
|
||||
v-if="selectedMemory"
|
||||
:memory-data="processedResults"
|
||||
:selected-memory-id="selectedMemoryId"
|
||||
:decay-rate="decayRate"
|
||||
:max-days-to-project="maxDaysToProject"
|
||||
:long-term-threshold="longTermThreshold"
|
||||
:muscle-memory-threshold="muscleMemoryThreshold"
|
||||
:joy-boost-factor="joyBoostFactor"
|
||||
:joy-decay-steepness="joyDecaySteepness"
|
||||
:aversion-spike-factor="aversionSpikeFactor"
|
||||
:aversion-stability="aversionStability"
|
||||
class="lg:col-span-2"
|
||||
/>
|
||||
|
||||
<!-- Memory Details -->
|
||||
<EmotionalMemoryDetail
|
||||
v-if="selectedMemory"
|
||||
:memory="selectedMemory"
|
||||
:long-term-threshold="longTermThreshold"
|
||||
:muscle-memory-threshold="muscleMemoryThreshold"
|
||||
@retrieve="handleRetrieval"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<label class="mb-1 block text-sm font-medium">Heatmap Time Range (days)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Range
|
||||
v-model="heatmapTimeRange"
|
||||
:min="7"
|
||||
:max="120"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
/>
|
||||
<span class="w-12 text-right font-mono">{{ heatmapTimeRange }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Controls -->
|
||||
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
|
||||
<EmotionalSettings
|
||||
v-model:joy-boost-factor="joyBoostFactor"
|
||||
v-model:joy-decay-steepness="joyDecaySteepness"
|
||||
v-model:aversion-spike-factor="aversionSpikeFactor"
|
||||
v-model:aversion-stability="aversionStability"
|
||||
v-model:random-recall-probability="randomRecallProbability"
|
||||
v-model:flashback-intensity="flashbackIntensity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Only show table if viewMode is table or combined -->
|
||||
<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">
|
||||
ID
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Base Score
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Joy Score
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Aversion Score
|
||||
</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">
|
||||
Age (days)
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
|
||||
Effective 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 v-if="processedResults.length" class="bg-white divide-y divide-neutral-200 dark:bg-neutral-900 dark:divide-neutral-800">
|
||||
<tr
|
||||
v-for="(memory, idx) in processedResults"
|
||||
:key="memory.id"
|
||||
class="cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': memory.id === selectedMemoryId }"
|
||||
@click="selectedMemoryId = memory.id"
|
||||
>
|
||||
<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">
|
||||
{{ memory.id }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ Math.round(memory.score) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
<span :class="memory.joy_score > 0.5 ? 'text-yellow-600 dark:text-yellow-400' : ''">
|
||||
{{ Math.round(memory.joy_score * 100) }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
<span :class="memory.aversion_score > 0.5 ? 'text-red-600 dark:text-red-400' : ''">
|
||||
{{ Math.round(memory.aversion_score * 100) }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ memory.retrieval_count }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm">
|
||||
{{ Math.round(memory.age_in_seconds / (24 * 60 * 60)) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-sm font-medium">
|
||||
{{ Math.round(memory.decayed_score) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<button
|
||||
class="rounded-lg bg-blue-100 px-3 py-1 text-xs font-medium dark:bg-blue-900"
|
||||
@click.stop="handleRetrieval(memory.id)"
|
||||
>
|
||||
Retrieve
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else class="bg-white dark:bg-neutral-900">
|
||||
<tr>
|
||||
<td colspan="9" class="px-4 py-8 text-center text-neutral-500">
|
||||
No memory data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SQL Query Preview (Optional for debugging) -->
|
||||
<div v-if="showAdvancedSettings" class="mt-6 max-w-full rounded-xl bg-neutral-50 dark:bg-neutral-800">
|
||||
<div class="overflow-x-scroll rounded bg-neutral-800 p-4 text-sm text-neutral-200">
|
||||
<pre class="whitespace-pre-wrap">
|
||||
<code>{{ emotionalDecayQuery }}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export interface EmotionalMemoryItem {
|
||||
// Basic memory properties
|
||||
id: string
|
||||
score: number
|
||||
decayed_score: number
|
||||
updated_at: string
|
||||
last_retrieved_at: string
|
||||
retrieval_count: number
|
||||
|
||||
// Emotional components
|
||||
joy_score: number
|
||||
aversion_score: number
|
||||
|
||||
// Timing information
|
||||
updated_at_str?: string
|
||||
last_retrieved_str?: string
|
||||
age_in_seconds: number
|
||||
time_since_retrieval: number
|
||||
|
||||
// Memory status factors
|
||||
ltm_factor?: number // Long-term memory factor (0-1)
|
||||
|
||||
// Optional calculated components for display
|
||||
age_in_days?: number
|
||||
effective_score?: number
|
||||
}
|
||||
|
||||
export interface EmotionalRetrievalModifiers {
|
||||
joyModifier: number
|
||||
aversionModifier: number
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
@@ -20,20 +19,19 @@ const props = withDefaults(defineProps<{
|
||||
trackValueColor: 'red',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
const modelValue = defineModel<number>('modelValue', { required: true })
|
||||
|
||||
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()
|
||||
const sliderValue = computed({
|
||||
get: () => modelValue.value * 10000,
|
||||
set: (value: number) => {
|
||||
modelValue.value = value / 10000
|
||||
updateTrackColor()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -58,7 +56,7 @@ function updateTrackColor() {
|
||||
:min="scaledMin"
|
||||
:max="scaledMax"
|
||||
:step="scaledStep"
|
||||
class="form_input-range slider-progress"
|
||||
class="slider-progress form_input-range"
|
||||
@input="(e) => {
|
||||
(e.target as HTMLInputElement).style.setProperty('--value', (e.target as HTMLInputElement).value)
|
||||
}"
|
||||
|
||||
Reference in New Issue
Block a user