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,11 +2475,13 @@ $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"
|
||||||
>
|
>
|
||||||
|
{/* Left Side - Original Previous Note */}
|
||||||
|
<div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto">
|
||||||
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
|
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
|
||||||
<div>
|
<div>
|
||||||
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`}
|
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`}
|
||||||
@@ -2419,6 +2552,37 @@ $current`);
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side - AI Suggestions for Previous Note */}
|
||||||
|
<div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto">
|
||||||
|
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center justify-between`}>
|
||||||
|
<div>AI Suggestions</div>
|
||||||
|
{previousNoteCorrectedContent && previousNoteCorrectedContent !== previousNote?.content && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (previousNote) {
|
||||||
|
const updatedNote = { ...previousNote, content: previousNoteCorrectedContent };
|
||||||
|
setPreviousNote(updatedNote);
|
||||||
|
setIsPreviousNoteModified(true);
|
||||||
|
setPreviousNoteCorrectedContent('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
className={`${getTextClass('base')}`}
|
||||||
|
>
|
||||||
|
ACCEPT
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={previousNoteCorrectedContent || (previousNote ? previousNote.content : '')}
|
||||||
|
readOnly
|
||||||
|
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>
|
||||||
|
|
||||||
{/* Current Note - Bottom Half */}
|
{/* Current Note - Bottom Half */}
|
||||||
<div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-hidden">
|
<div className="flex-1 bg-card rounded-lg border border-border shadow-sm p-6 overflow-hidden">
|
||||||
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
|
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
|
||||||
|
Reference in New Issue
Block a user