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:
143
keep-notes/tests/search/non-regression.spec.ts
Normal file
143
keep-notes/tests/search/non-regression.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Non-regression tests for semantic search
|
||||
* These tests ensure that increasing the threshold from 0.40 to 0.65
|
||||
* doesn't lose valid positive matches
|
||||
*/
|
||||
test.describe('Search Non-Regression Tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should find notes with exact keyword matches', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('test');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const results = await page.locator('.note-card').count();
|
||||
|
||||
// Even with higher semantic threshold, exact keyword matches should always appear
|
||||
if (results > 0) {
|
||||
const titles = await page.locator('.note-card .note-title').allTextContents();
|
||||
const hasExactMatch = titles.some(title =>
|
||||
title.toLowerCase().includes('test')
|
||||
);
|
||||
expect(hasExactMatch).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('should find notes with title matches', async ({ page }) => {
|
||||
// Search for something that should be in titles
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
|
||||
// Try a generic search term
|
||||
await searchInput.fill('note');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const results = await page.locator('.note-card');
|
||||
const count = await results.count();
|
||||
|
||||
// Should find results with "note" in them
|
||||
if (count > 0) {
|
||||
const allText = await results.allTextContents();
|
||||
const hasNote = allText.some(text =>
|
||||
text.toLowerCase().includes('note')
|
||||
);
|
||||
expect(hasNote).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle multi-word queries correctly', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
|
||||
// Multi-word search should work
|
||||
await searchInput.fill('test note');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should not error and should return results
|
||||
const results = await page.locator('.note-card').count();
|
||||
expect(results).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should be case-insensitive', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
|
||||
// Search with uppercase
|
||||
await searchInput.fill('TEST');
|
||||
await page.waitForTimeout(500);
|
||||
const upperCount = await page.locator('.note-card').count();
|
||||
|
||||
// Search with lowercase
|
||||
await searchInput.fill('test');
|
||||
await page.waitForTimeout(500);
|
||||
const lowerCount = await page.locator('.note-card').count();
|
||||
|
||||
// Should return same results
|
||||
expect(upperCount).toBe(lowerCount);
|
||||
});
|
||||
|
||||
test('should handle empty search gracefully', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
|
||||
// Empty search should show all notes or handle gracefully
|
||||
await searchInput.fill('');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should not error
|
||||
const results = await page.locator('.note-card').count();
|
||||
expect(results).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should clear search results', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
|
||||
// Search for something
|
||||
await searchInput.fill('temporary search');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Clear search
|
||||
await searchInput.fill('');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should return to showing all notes (or initial state)
|
||||
const results = await page.locator('.note-card').count();
|
||||
expect(results).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Unit tests for threshold behavior
|
||||
*/
|
||||
test.describe('Threshold Behavior Unit Tests', () => {
|
||||
test('threshold 0.65 should filter more than 0.40', () => {
|
||||
// Simulate semantic scores
|
||||
const scores = [0.80, 0.65, 0.50, 0.45, 0.40, 0.30, 0.20];
|
||||
|
||||
// Old threshold (0.40)
|
||||
const oldThresholdMatches = scores.filter(s => s > 0.40).length;
|
||||
|
||||
// New threshold (0.65)
|
||||
const newThresholdMatches = scores.filter(s => s > 0.65).length;
|
||||
|
||||
// New threshold should filter more strictly
|
||||
expect(newThresholdMatches).toBeLessThan(oldThresholdMatches);
|
||||
});
|
||||
|
||||
test('threshold 0.65 should keep high-quality matches', () => {
|
||||
const highQualityScores = [0.95, 0.85, 0.75, 0.70, 0.68];
|
||||
|
||||
// All high-quality scores should pass the threshold
|
||||
const matches = highQualityScores.filter(s => s > 0.65);
|
||||
expect(matches.length).toBe(highQualityScores.length);
|
||||
});
|
||||
|
||||
test('threshold 0.65 should filter low-quality matches', () => {
|
||||
const lowQualityScores = [0.30, 0.40, 0.50, 0.60, 0.64];
|
||||
|
||||
// None should pass the 0.65 threshold
|
||||
const matches = lowQualityScores.filter(s => s > 0.65);
|
||||
expect(matches.length).toBe(0);
|
||||
});
|
||||
});
|
||||
125
keep-notes/tests/search/semantic-threshold.spec.ts
Normal file
125
keep-notes/tests/search/semantic-threshold.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Semantic Search Threshold Tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Login before each test
|
||||
await page.goto('http://localhost:3000');
|
||||
// Wait for potential redirect or login
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should use default threshold of 0.65 for semantic matching', async ({ page }) => {
|
||||
// This test verifies that the default threshold is applied
|
||||
// We'll search for something and check that only relevant results appear
|
||||
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
// Get initial note count
|
||||
const initialNotes = await page.locator('.note-card').count();
|
||||
|
||||
// Search for something specific
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('programming code');
|
||||
await page.waitForTimeout(500); // Wait for search debounce
|
||||
|
||||
// Get search results
|
||||
const searchResults = await page.locator('.note-card').count();
|
||||
|
||||
// With threshold 0.65, we should have fewer false positives
|
||||
// This is a basic sanity check - exact assertions depend on your data
|
||||
expect(searchResults).toBeLessThanOrEqual(initialNotes);
|
||||
|
||||
// Clear search
|
||||
await searchInput.fill('');
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('should not show unrelated notes when searching for specific terms', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
// Search for technical terms
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('javascript programming');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Get all search result titles
|
||||
const resultTitles = await page.locator('.note-card .note-title').allTextContents();
|
||||
|
||||
// Check that results are related to the search (basic keyword check)
|
||||
// With higher threshold (0.65), semantic matches should be more relevant
|
||||
const hasIrrelevantResults = resultTitles.some(title => {
|
||||
const lowerTitle = title.toLowerCase();
|
||||
// If a result mentions completely unrelated topics like cooking/food
|
||||
// when searching for programming, that's a false positive
|
||||
return lowerTitle.includes('recipe') ||
|
||||
lowerTitle.includes('cooking') ||
|
||||
lowerTitle.includes('food') ||
|
||||
lowerTitle.includes('shopping');
|
||||
});
|
||||
|
||||
// With threshold 0.65, we expect fewer false positives
|
||||
// This test may need adjustment based on your actual data
|
||||
expect(hasIrrelevantResults).toBe(false);
|
||||
});
|
||||
|
||||
test('should still show highly relevant semantic matches', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
// Search for a concept and its close synonyms
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('coding');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Get search results
|
||||
const searchResults = await page.locator('.note-card');
|
||||
const count = await searchResults.count();
|
||||
|
||||
// Even with higher threshold, we should find semantic matches
|
||||
// for closely related terms like "programming", "development", etc.
|
||||
if (count > 0) {
|
||||
const titles = await page.locator('.note-card .note-title').allTextContents();
|
||||
const hasRelevantContent = titles.some(title => {
|
||||
const lowerTitle = title.toLowerCase();
|
||||
return lowerTitle.includes('code') ||
|
||||
lowerTitle.includes('program') ||
|
||||
lowerTitle.includes('develop') ||
|
||||
lowerTitle.includes('software') ||
|
||||
lowerTitle.includes('app');
|
||||
});
|
||||
|
||||
// Should find semantically related content
|
||||
expect(hasRelevantContent).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('threshold can be configured via SystemConfig', async ({ page }) => {
|
||||
// This is an integration test verifying the configuration system
|
||||
// In a real scenario, you'd make an API call to update the config
|
||||
|
||||
// Default threshold should be 0.65
|
||||
const response = await page.request.get('/api/admin/embeddings/validate');
|
||||
|
||||
// This endpoint requires admin auth, so we're just checking it exists
|
||||
// The actual threshold value is tested in unit tests
|
||||
expect(response.ok()).toBeFalsy(); // Should be 401/403 without auth
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Semantic Threshold Unit Tests', () => {
|
||||
test('default threshold should be 0.65', () => {
|
||||
// This is a compile-time check
|
||||
// The actual value is in SEARCH_DEFAULTS.SEMANTIC_THRESHOLD
|
||||
const { SEARCH_DEFAULTS } = require('../../lib/config');
|
||||
expect(SEARCH_DEFAULTS.SEMANTIC_THRESHOLD).toBe(0.65);
|
||||
});
|
||||
|
||||
test('threshold should be higher than old value', () => {
|
||||
const { SEARCH_DEFAULTS } = require('../../lib/config');
|
||||
expect(SEARCH_DEFAULTS.SEMANTIC_THRESHOLD).toBeGreaterThan(0.40);
|
||||
});
|
||||
|
||||
test('threshold should be less than 1.0', () => {
|
||||
const { SEARCH_DEFAULTS } = require('../../lib/config');
|
||||
expect(SEARCH_DEFAULTS.SEMANTIC_THRESHOLD).toBeLessThan(1.0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user