fix: improve note interactions and markdown LaTeX support
## Bug Fixes ### Note Card Actions - Fix broken size change functionality (missing state declaration) - Implement React 19 useOptimistic for instant UI feedback - Add startTransition for non-blocking updates - Ensure smooth animations without page refresh - All note actions now work: pin, archive, color, size, checklist ### Markdown LaTeX Rendering - Add remark-math and rehype-katex plugins - Support inline equations with dollar sign syntax - Support block equations with double dollar sign syntax - Import KaTeX CSS for proper styling - Equations now render correctly instead of showing raw LaTeX ## Technical Details - Replace undefined currentNote references with optimistic state - Add optimistic updates before server actions for instant feedback - Use router.refresh() in transitions for smart cache invalidation - Install remark-math, rehype-katex, and katex packages ## Testing - Build passes successfully with no TypeScript errors - Dev server hot-reloads changes correctly
This commit is contained in:
152
keep-notes/tests/unit/rrf.test.ts
Normal file
152
keep-notes/tests/unit/rrf.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { calculateRRFK } from '../../lib/utils';
|
||||
|
||||
test.describe('RRF K Calculation Tests', () => {
|
||||
test('should return minimum k=20 for small datasets', () => {
|
||||
// For small datasets (< 200 notes), k should be 20
|
||||
expect(calculateRRFK(0)).toBe(20);
|
||||
expect(calculateRRFK(10)).toBe(20);
|
||||
expect(calculateRRFK(50)).toBe(20);
|
||||
expect(calculateRRFK(100)).toBe(20);
|
||||
expect(calculateRRFK(199)).toBe(20);
|
||||
});
|
||||
|
||||
test('should return k=20 for exactly 200 notes', () => {
|
||||
expect(calculateRRFK(200)).toBe(20);
|
||||
});
|
||||
|
||||
test('should scale k for larger datasets', () => {
|
||||
// k = max(20, totalNotes / 10)
|
||||
expect(calculateRRFK(500)).toBe(50); // 500/10 = 50
|
||||
expect(calculateRRFK(1000)).toBe(100); // 1000/10 = 100
|
||||
expect(calculateRRFK(250)).toBe(25); // 250/10 = 25
|
||||
});
|
||||
|
||||
test('should handle edge cases', () => {
|
||||
expect(calculateRRFK(201)).toBe(20); // 201/10 = 20.1 → floor → 20, max(20,20) = 20
|
||||
expect(calculateRRFK(210)).toBe(21); // 210/10 = 21
|
||||
expect(calculateRRFK(10000)).toBe(1000); // 10000/10 = 1000
|
||||
});
|
||||
|
||||
test('k should always be at least 20', () => {
|
||||
// Even for very small datasets, k should not be below 20
|
||||
for (let i = 0; i <= 200; i++) {
|
||||
expect(calculateRRFK(i)).toBeGreaterThanOrEqual(20);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be lower than old value for typical datasets', () => {
|
||||
// For typical user datasets (< 500 notes), new k should be lower than old k=60
|
||||
expect(calculateRRFK(100)).toBeLessThan(60);
|
||||
expect(calculateRRFK(200)).toBeLessThan(60);
|
||||
expect(calculateRRFK(300)).toBe(30); // 300/10 = 30
|
||||
expect(calculateRRFK(500)).toBe(50); // 500/10 = 50
|
||||
});
|
||||
|
||||
test('should surpass old value for very large datasets', () => {
|
||||
// For very large datasets (> 600 notes), k should be higher than 60
|
||||
expect(calculateRRFK(700)).toBe(70); // 700/10 = 70
|
||||
expect(calculateRRFK(1000)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('RRF Ranking Behavior Tests', () => {
|
||||
test('RRF with lower k penalizes low ranks more', () => {
|
||||
// Simulate RRF scores for a note at rank 5
|
||||
// Formula: score = 1 / (k + rank)
|
||||
|
||||
const rank = 5;
|
||||
const scoreWithK20 = 1 / (20 + rank); // 1/25 = 0.04
|
||||
const scoreWithK60 = 1 / (60 + rank); // 1/65 = 0.015
|
||||
|
||||
// Lower k gives higher score to low ranks
|
||||
expect(scoreWithK20).toBeGreaterThan(scoreWithK60);
|
||||
});
|
||||
|
||||
test('RRF favors items ranked high in both lists', () => {
|
||||
// Note A: rank 1 in both lists
|
||||
const scoreA_K20 = 1 / (20 + 1) + 1 / (20 + 1); // 2/21 ≈ 0.095
|
||||
|
||||
// Note B: rank 1 in one list, rank 10 in other
|
||||
const scoreB_K20 = 1 / (20 + 1) + 1 / (20 + 10); // 1/21 + 1/30 ≈ 0.081
|
||||
|
||||
// Note C: rank 5 in both lists
|
||||
const scoreC_K20 = 1 / (20 + 5) + 1 / (20 + 5); // 2/25 = 0.08
|
||||
|
||||
// Note A should have highest score (consistently high)
|
||||
expect(scoreA_K20).toBeGreaterThan(scoreB_K20);
|
||||
expect(scoreA_K20).toBeGreaterThan(scoreC_K20);
|
||||
});
|
||||
|
||||
test('k=20 vs k=60 ranking difference', () => {
|
||||
// Note at rank 20
|
||||
const rank = 20;
|
||||
|
||||
const scoreWithK20 = 1 / (20 + rank); // 1/40 = 0.025
|
||||
const scoreWithK60 = 1 / (60 + rank); // 1/80 = 0.0125
|
||||
|
||||
// With k=20, rank 20 is scored 2x higher than with k=60
|
||||
expect(scoreWithK20 / scoreWithK60).toBeCloseTo(2.0, 1);
|
||||
});
|
||||
|
||||
test('RRF score should decrease as rank increases', () => {
|
||||
const k = 20;
|
||||
|
||||
const scoreRank1 = 1 / (k + 1);
|
||||
const scoreRank5 = 1 / (k + 5);
|
||||
const scoreRank10 = 1 / (k + 10);
|
||||
const scoreRank50 = 1 / (k + 50);
|
||||
|
||||
expect(scoreRank1).toBeGreaterThan(scoreRank5);
|
||||
expect(scoreRank5).toBeGreaterThan(scoreRank10);
|
||||
expect(scoreRank10).toBeGreaterThan(scoreRank50);
|
||||
});
|
||||
|
||||
test('RRF handles missing ranks gracefully', () => {
|
||||
// If a note is not in a list, it gets the max rank
|
||||
const k = 20;
|
||||
const totalNotes = 100;
|
||||
const missingRank = totalNotes; // Treated as worst rank
|
||||
|
||||
const scoreWithMissing = 1 / (k + missingRank);
|
||||
const scoreWithRank50 = 1 / (k + 50);
|
||||
|
||||
// Missing rank should give much lower score
|
||||
expect(scoreWithMissing).toBeLessThan(scoreWithRank50);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('RRF Adaptive Behavior Tests', () => {
|
||||
test('adaptive k provides better rankings for small datasets', () => {
|
||||
// For 50 notes: k=20 (adaptive) vs k=60 (old)
|
||||
const totalNotes = 50;
|
||||
const kAdaptive = calculateRRFK(totalNotes); // 20
|
||||
const kOld = 60;
|
||||
|
||||
// Compare scores for rank 10 (20% of dataset)
|
||||
const rank = 10;
|
||||
const scoreAdaptive = 1 / (kAdaptive + rank);
|
||||
const scoreOld = 1 / (kOld + rank);
|
||||
|
||||
// Adaptive k gives higher score to mid-rank items in small datasets
|
||||
expect(scoreAdaptive).toBeGreaterThan(scoreOld);
|
||||
});
|
||||
|
||||
test('adaptive k scales appropriately', () => {
|
||||
const datasets = [10, 50, 100, 200, 500, 1000];
|
||||
|
||||
datasets.forEach(notes => {
|
||||
const k = calculateRRFK(notes);
|
||||
|
||||
// k should always be at least 20
|
||||
expect(k).toBeGreaterThanOrEqual(20);
|
||||
|
||||
// k should scale with dataset size
|
||||
if (notes < 200) {
|
||||
expect(k).toBe(20);
|
||||
} else {
|
||||
expect(k).toBe(Math.floor(notes / 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user