Restores two pieces of vanilla behavior the React port silently dropped: 1. The output divs were contenteditable in vanilla — clinicians fix AI mistakes inline before saving the encounter or copying out. The early React port rendered the output as a read-only div, so any edit forced a copy-paste workflow. 2. correctionTracker.js (public/js/correctionTracker.js) saved every meaningful inline edit to user_memories under category correction_<section> so the AI learns user preferences. Settings → Corrections still showed them, but nothing in React was *writing* them — the loop was broken. New EditableResult component wraps the result body in a textarea, captures the AI baseline on first render / after refine|shorten, and on blur posts to /api/memories/correction (only when the edit clears the noise threshold from vanilla — wordDiff ≥ 2 OR charDiff ≥ 20 on outputs > 100 chars). Wired into all 7 note pages: Encounter → section: 'encounter' Dictation → section: 'hpi' SOAP → section: 'soap' SickVisit → section: 'sickvisit' WellVisit → section: 'wellvisit' HospitalCourse → section: null (server enum has no correction_hospital) ChartReview → section: null (server enum has no correction_chart) Tests: shared/clinical/correction-tracker.ts (+ .test.ts) — pure heuristic with vectors covering the noise floor (single-word swap on long text → skipped; new sentence → tracked; large char diff → tracked). Lives in shared/ so the root vitest config picks it up; the React component imports from there.
50 lines
2.3 KiB
TypeScript
50 lines
2.3 KiB
TypeScript
// Tests for the correction-tracking heuristic — should not flood the
|
|
// memories table with whitespace-only or single-word fixes on long
|
|
// outputs, but must capture all real edits.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { isMeaningfulChange } from './correction-tracker';
|
|
|
|
describe('isMeaningfulChange', () => {
|
|
it('returns false for empty strings', () => {
|
|
expect(isMeaningfulChange('', 'anything')).toBe(false);
|
|
expect(isMeaningfulChange('anything', '')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the trimmed text is identical', () => {
|
|
expect(isMeaningfulChange('hello', 'hello')).toBe(false);
|
|
expect(isMeaningfulChange('hello\n', ' hello ')).toBe(false);
|
|
});
|
|
|
|
it('returns true for short edits even with small word/char diffs', () => {
|
|
// The 100-char threshold guards against noise on LONG outputs only.
|
|
// Short edits always count.
|
|
expect(isMeaningfulChange('hi', 'hi.')).toBe(true);
|
|
expect(isMeaningfulChange('hi', 'hello')).toBe(true);
|
|
});
|
|
|
|
it('rejects single-word, ≤20-char edits on long original output', () => {
|
|
const long = 'Patient is a 5-year-old male presenting with fever for two days. ' +
|
|
'No respiratory symptoms. No GI symptoms. Vitals stable. Exam unremarkable. ' +
|
|
'Plan: supportive care, return precautions reviewed.';
|
|
// Same length, swap one word — wordDiff=0, charDiff=0 → noise, skip.
|
|
expect(isMeaningfulChange(long, long.replace('male', 'mail'))).toBe(false);
|
|
// Same length, swap one word for similar length — still noise, skip.
|
|
expect(isMeaningfulChange(long, long.replace('two days', 'one day '))).toBe(false);
|
|
});
|
|
|
|
it('accepts multi-word edits even on long output', () => {
|
|
const long = 'Patient is a 5-year-old male presenting with fever for two days. ' +
|
|
'No respiratory symptoms. No GI symptoms. Vitals stable. Exam unremarkable. ' +
|
|
'Plan: supportive care, return precautions reviewed.';
|
|
// Inserting a sentence is a real edit — track it.
|
|
const edited = long + ' Will follow up in 48 hours if symptoms persist.';
|
|
expect(isMeaningfulChange(long, edited)).toBe(true);
|
|
});
|
|
|
|
it('accepts single-word edits when the char diff is large enough', () => {
|
|
const long = 'a'.repeat(150);
|
|
// 30-char insertion — over the 20-char noise floor.
|
|
expect(isMeaningfulChange(long, long + 'x'.repeat(30))).toBe(true);
|
|
});
|
|
});
|