feat(Index.tsx): implement debounced text correction using Ollama API
This commit is contained in:
@@ -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 */}
|
||||||
|
Reference in New Issue
Block a user