diff --git a/client/src/components/EditableResult.tsx b/client/src/components/EditableResult.tsx new file mode 100644 index 0000000..6f0b148 --- /dev/null +++ b/client/src/components/EditableResult.tsx @@ -0,0 +1,117 @@ +// ============================================================ +// EditableResult — editable AI-output box + correction tracking + +// OutputActions toolbar. Restores two pieces of vanilla behavior +// the React migration silently dropped: +// +// 1. The output divs were `contenteditable="true"` in vanilla. +// Users routinely fix AI mistakes inline before saving the +// encounter or copying out. The early React port rendered the +// output as a read-only div — copy was the only path. +// +// 2. correctionTracker.js (public/js/correctionTracker.js) saved +// every meaningful inline edit to user_memories under category +// `correction_
` so the AI learns user preferences. +// Settings → Corrections still shows them, but nothing in React +// was *writing* them. This component restores that loop. +// +// On blur, if the text differs from the captured "original AI +// output" by more than the noise threshold, POST /api/memories/ +// correction. Mirrors the vanilla heuristic exactly so we don't +// flood memories with whitespace-only edits. +// ============================================================ + +import { useEffect, useRef } from 'react'; +import OutputActions from '@/components/OutputActions'; +import { api } from '@/lib/api'; +import { isMeaningfulChange } from '@shared/clinical/correction-tracker'; + +// Server enum — must match VALID_CATEGORIES in src/routes/memories.ts. +export type CorrectionSection = + | 'encounter' + | 'hpi' + | 'soap' + | 'wellvisit' + | 'sickvisit'; + +interface Props { + text: string; + onChange: (next: string) => void; + /** + * Which note type this is — selects the correction memory bucket. + * Pass `null` to skip correction tracking (Hospital Course and Chart + * Review aren't in the server's VALID_CATEGORIES enum). + */ + section: CorrectionSection | null; + /** Filename prefix for Nextcloud export (passed through to OutputActions). */ + exportLabel: string; + /** docType field for Nextcloud export. */ + exportType: string; + /** Optional source material — refine reads this for context. */ + sourceContext?: string; + /** Title shown in the section header. */ + title: string; +} + +export default function EditableResult({ + text, onChange, section, exportLabel, exportType, sourceContext, title, +}: Props) { + // The "AI baseline" — what the model produced last. Updated when refine + // or shorten replaces the body (handled in onUpdate below) so the next + // user edit is measured against the new baseline, not the original. + const originalRef = useRef(text); + // Re-baseline whenever the text grows/changes from a non-edit source — + // i.e. when generation produced new output (parent flipped from null→text). + // We can't perfectly distinguish a parent-driven change from a user typing, + // but the typical generation flow is null → full text, so a length jump + // back to a different value is treated as a new baseline. + useEffect(() => { + if (!originalRef.current && text) originalRef.current = text; + }, [text]); + + async function maybeSaveCorrection() { + if (!section) return; + if (!isMeaningfulChange(originalRef.current, text)) return; + try { + await api.post('/api/memories/correction', { + section, + original_snippet: originalRef.current, + corrected_snippet: text, + }); + // Successful save → make the new text the baseline so successive + // edits get tracked against the most-recently-saved version. + originalRef.current = text; + } catch { + // Fail silently — correction tracking is best-effort, never blocks + // the user's primary copy/refine/save flow. + } + } + + return ( +
+
+

{title}

+
+