feat(Index.tsx): implement debounced text correction using Ollama API

This commit is contained in:
2025-08-29 18:04:59 +02:00
parent d4dfe11cef
commit 333d44e621

View File

@@ -121,6 +121,9 @@ Keep tags concise, use lowercase, and separate words with hyphens if needed.`);
$current`); $current`);
const [contextSize, setContextSize] = useState(3); const [contextSize, setContextSize] = useState(3);
const [correctedContent, setCorrectedContent] = useState('');
const [previousNoteCorrectedContent, setPreviousNoteCorrectedContent] = useState('');
const [correctionTimeout, setCorrectionTimeout] = useState<NodeJS.Timeout>();
const { resolvedTheme, setTheme } = useTheme(); const { resolvedTheme, setTheme } = useTheme();
@@ -309,6 +312,124 @@ $current`);
setTagGenerationTimeout(timeout); setTagGenerationTimeout(timeout);
}; };
// Debounced text correction for current note
const debouncedCorrectText = (content: string) => {
if (correctionTimeout) {
clearTimeout(correctionTimeout);
}
const timeout = setTimeout(async () => {
if (content.trim()) {
try {
addDebugInfo('Correcting current note text...');
const corrected = await correctText(content);
setCorrectedContent(corrected);
} catch (error) {
console.error('Text correction failed:', error);
setCorrectedContent('');
}
} else {
setCorrectedContent('');
}
}, 500);
setCorrectionTimeout(timeout);
};
// Debounced text correction for previous note
const debouncedCorrectPreviousNote = (content: string) => {
if (correctionTimeout) {
clearTimeout(correctionTimeout);
}
const timeout = setTimeout(async () => {
if (content.trim()) {
try {
addDebugInfo('Correcting previous note text...');
const corrected = await correctText(content);
setPreviousNoteCorrectedContent(corrected);
} catch (error) {
console.error('Previous note text correction failed:', error);
setPreviousNoteCorrectedContent('');
}
} else {
setPreviousNoteCorrectedContent('');
}
}, 500);
setCorrectionTimeout(timeout);
};
// Correct text using Ollama
const correctText = async (content: string): Promise<string> => {
try {
const response = await fetchWithTiming(`${ollamaEndpoint}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: ollamaModel,
system: `You are a helpful assistant that corrects ONLY spelling and punctuation errors in text.
Your task is to fix obvious spelling mistakes and punctuation errors.
DO NOT change any words, phrases, or meaning.
DO NOT replace informal language, slang, or profanity.
DO NOT rephrase or rewrite anything.
Preserve the exact original tone, style, and meaning.
Only fix clear spelling errors and punctuation mistakes.
Return ONLY the corrected text, no explanations.`,
prompt: `Fix only spelling and punctuation errors in this text. Do not change any words or meaning:
${content}`,
stream: false,
keep_alive: ollamaKeepAlive,
temperature: 0.1,
}),
}, 'Correct Text');
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Ollama API error ${response.status}: ${errorText}`);
}
const data = await response.json();
const correctedText = data.response?.trim();
if (!correctedText) {
throw new Error('Empty response from Ollama - check if model is loaded');
}
addDebugInfo(`Text corrected successfully`);
return correctedText;
} catch (error) {
console.error('Error correcting text:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
let userMessage = 'Failed to correct text';
if (error instanceof TypeError && error.message.includes('fetch')) {
addDebugInfo('Text correction failed: Network error - Ollama not reachable');
userMessage = 'Ollama not reachable - check if it\'s running';
} else if (errorMessage.includes('Failed to fetch')) {
addDebugInfo('Text correction failed: Ollama connection refused');
userMessage = 'Ollama connection refused - check if Ollama is running';
} else {
addDebugInfo(`Text correction failed: ${errorMessage}`);
userMessage = `Text correction failed: ${errorMessage}`;
}
toast({
title: "Text Correction Failed",
description: userMessage,
variant: "destructive",
});
return content; // Return original content if correction fails
}
};
// Generate tags using Ollama // Generate tags using Ollama
const generateTags = async (content: string, noteIndex?: number): Promise<string[]> => { const generateTags = async (content: string, noteIndex?: number): Promise<string[]> => {
try { try {
@@ -1051,6 +1172,12 @@ $current`);
setCurrentNoteIndex(newIndex); setCurrentNoteIndex(newIndex);
setPreviousNote(noteCache[newIndex]); setPreviousNote(noteCache[newIndex]);
setIsPreviousNoteModified(false); setIsPreviousNoteModified(false);
setPreviousNoteCorrectedContent(''); // Clear corrected content when note changes
// Automatically load AI corrections for the new note
if (noteCache[newIndex].content.trim()) {
debouncedCorrectPreviousNote(noteCache[newIndex].content);
}
} }
// Load more notes if we're getting close to the end // Load more notes if we're getting close to the end
@@ -1224,6 +1351,10 @@ $current`);
if (tagGenerationTimeout) { if (tagGenerationTimeout) {
clearTimeout(tagGenerationTimeout); clearTimeout(tagGenerationTimeout);
} }
// Clear any pending correction timeout on cleanup
if (correctionTimeout) {
clearTimeout(correctionTimeout);
}
}; };
}, [currentNote, previousNote, scratchPad, isPreviousNoteModified, isSearchOpen, isGotoOpen, isCleanupOpen]); }, [currentNote, previousNote, scratchPad, isPreviousNoteModified, isSearchOpen, isGotoOpen, isCleanupOpen]);
@@ -2344,79 +2475,112 @@ $current`);
<div className="flex-1 flex p-4 gap-4 overflow-hidden"> <div className="flex-1 flex p-4 gap-4 overflow-hidden">
{/* Left Panel - 70% */} {/* Left Panel - 70% */}
<div className="flex-[7] flex flex-col gap-4 h-[calc(100vh-8rem)] overflow-hidden"> <div className="flex-[7] flex flex-col gap-4 h-[calc(100vh-8rem)] overflow-hidden">
{/* Previous Note - Top Half */} {/* Previous Note - Top Half - Split Layout */}
<div <div
ref={previousNoteRef} ref={previousNoteRef}
className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto cursor-pointer select-none" className="flex-1 flex gap-4 overflow-hidden cursor-pointer select-none"
> >
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}> {/* Left Side - Original Previous Note */}
<div> <div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto">
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`} <div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
<div>
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`}
</div>
{previousNote && (
<div className="flex-1 flex items-center gap-2">
<span className={`${getTextClass('base')} text-muted-foreground`}>Tags:</span>
<input
type="text"
value={(previousNote.tags || []).join(', ')}
onChange={(e) => {
const updatedNote = { ...previousNote, tags: [e.target.value] };
setPreviousNote(updatedNote);
setIsPreviousNoteModified(true);
}}
className={`flex-1 border-0 bg-transparent ${getTextClass('base')} p-0 outline-none`}
/>
{!autoGenerateTags && (
<Button
onClick={async () => {
if (previousNote) {
addDebugInfo('Manually generating tags...');
const tags = await generateTags(previousNote.content, currentNoteIndex);
const updatedNote = { ...previousNote, tags };
setPreviousNote(updatedNote);
setIsPreviousNoteModified(true);
}
}}
size="sm"
variant="outline"
className={`${getTextClass('base')} px-2 py-1 h-6`}
>
AI
</Button>
)}
</div>
)}
</div> </div>
{previousNote && ( {previousNote ? (
<div className="flex-1 flex items-center gap-2"> <div className="space-y-4 h-[calc(100%-2rem)]">
<span className={`${getTextClass('base')} text-muted-foreground`}>Tags:</span> <div className={`${getTextClass('xl')} text-muted-foreground`}>
<input {formatDate(previousNote.epochTime)}
type="text" </div>
value={(previousNote.tags || []).join(', ')} <Textarea
value={previousNote.content}
onChange={(e) => { onChange={(e) => {
const updatedNote = { ...previousNote, tags: [e.target.value] }; const newContent = e.target.value;
const updatedNote = { ...previousNote, content: newContent };
setPreviousNote(updatedNote); setPreviousNote(updatedNote);
setIsPreviousNoteModified(true); setIsPreviousNoteModified(true);
// Debounced tag generation for previous notes
if (autoGenerateTags) {
debouncedGenerateTags(newContent, currentNoteIndex);
}
if (newContent.trim() === '') {
deleteNote(previousNote.id);
}
}} }}
className={`flex-1 border-0 bg-transparent ${getTextClass('base')} p-0 outline-none`} onBlur={handlePreviousNoteBlur}
className={`h-[calc(100%-3rem)] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
placeholder="Previous entry content..."
/> />
{!autoGenerateTags && ( </div>
<Button ) : (
onClick={async () => { <div className={`text-muted-foreground text-center py-12 ${getTextClass('2xl')}`}>
if (previousNote) { {isLoading ? 'Loading notes...' : 'No previous entries found'}
addDebugInfo('Manually generating tags...');
const tags = await generateTags(previousNote.content, currentNoteIndex);
const updatedNote = { ...previousNote, tags };
setPreviousNote(updatedNote);
setIsPreviousNoteModified(true);
}
}}
size="sm"
variant="outline"
className={`${getTextClass('base')} px-2 py-1 h-6`}
>
AI
</Button>
)}
</div> </div>
)} )}
</div> </div>
{previousNote ? (
<div className="space-y-4 h-[calc(100%-2rem)]"> {/* Right Side - AI Suggestions for Previous Note */}
<div className={`${getTextClass('xl')} text-muted-foreground`}> <div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto">
{formatDate(previousNote.epochTime)} <div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center justify-between`}>
</div> <div>AI Suggestions</div>
<Textarea {previousNoteCorrectedContent && previousNoteCorrectedContent !== previousNote?.content && (
value={previousNote.content} <Button
onChange={(e) => { onClick={() => {
const newContent = e.target.value; if (previousNote) {
const updatedNote = { ...previousNote, content: newContent }; const updatedNote = { ...previousNote, content: previousNoteCorrectedContent };
setPreviousNote(updatedNote); setPreviousNote(updatedNote);
setIsPreviousNoteModified(true); setIsPreviousNoteModified(true);
// Debounced tag generation for previous notes setPreviousNoteCorrectedContent('');
if (autoGenerateTags) { }
debouncedGenerateTags(newContent, currentNoteIndex); }}
} size="sm"
if (newContent.trim() === '') { variant="default"
deleteNote(previousNote.id); className={`${getTextClass('base')}`}
} >
}} ACCEPT
onBlur={handlePreviousNoteBlur} </Button>
className={`h-[calc(100%-3rem)] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`} )}
placeholder="Previous entry content..."
/>
</div> </div>
) : ( <Textarea
<div className={`text-muted-foreground text-center py-12 ${getTextClass('2xl')}`}> value={previousNoteCorrectedContent || (previousNote ? previousNote.content : '')}
{isLoading ? 'Loading notes...' : 'No previous entries found'} readOnly
</div> className={`h-[calc(100%-2rem)] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
)} placeholder={previousNote ? "AI is analyzing your text..." : "No previous entry to analyze"}
/>
</div>
</div> </div>
{/* Current Note - Bottom Half */} {/* Current Note - Bottom Half */}