feat(Index.tsx): refactor tag generation and display for notes to use comma-separated values and allow manual editing
This commit is contained in:
@@ -73,6 +73,7 @@ const Index = () => {
|
|||||||
console.log(`Component mounted after ${Date.now() - GLOBAL_START_TIME}ms`);
|
console.log(`Component mounted after ${Date.now() - GLOBAL_START_TIME}ms`);
|
||||||
|
|
||||||
const [currentNote, setCurrentNote] = useState('');
|
const [currentNote, setCurrentNote] = useState('');
|
||||||
|
const [currentNoteTags, setCurrentNoteTags] = useState<string[]>([]);
|
||||||
const [previousNote, setPreviousNote] = useState<Note | null>(null);
|
const [previousNote, setPreviousNote] = useState<Note | null>(null);
|
||||||
const [scratchPad, setScratchPad] = useState('');
|
const [scratchPad, setScratchPad] = useState('');
|
||||||
const [scratchId, setScratchId] = useState<string | null>(null);
|
const [scratchId, setScratchId] = useState<string | null>(null);
|
||||||
@@ -251,7 +252,7 @@ Focus on:
|
|||||||
- Problem domains or contexts
|
- Problem domains or contexts
|
||||||
- Key technical terms that someone might search for
|
- Key technical terms that someone might search for
|
||||||
|
|
||||||
Return ONLY a JSON array of strings, no other text. Example: ["golang", "testing", "array-comparison", "cmp-library"]
|
Return ONLY a comma-separated list of tags, no other text. Example: golang, testing, array-comparison, cmp-library
|
||||||
|
|
||||||
Keep tags concise, use lowercase, and separate words with hyphens if needed.`;
|
Keep tags concise, use lowercase, and separate words with hyphens if needed.`;
|
||||||
|
|
||||||
@@ -273,37 +274,25 @@ ${content}`;
|
|||||||
}, 'Generate Tags');
|
}, 'Generate Tags');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to generate tags');
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(`Ollama API error ${response.status}: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const responseText = data.response?.trim();
|
const responseText = data.response?.trim();
|
||||||
|
|
||||||
if (!responseText) {
|
if (!responseText) {
|
||||||
throw new Error('Empty response from Ollama');
|
throw new Error('Empty response from Ollama - check if model is loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse JSON from the response
|
// Parse CSV format from the response
|
||||||
try {
|
const tags = responseText
|
||||||
const tags = JSON.parse(responseText);
|
.split(',')
|
||||||
if (Array.isArray(tags) && tags.every(tag => typeof tag === 'string')) {
|
.map((tag: string) => tag.trim())
|
||||||
|
.filter((tag: string) => tag.length > 0);
|
||||||
|
|
||||||
addDebugInfo(`Generated ${tags.length} tags: ${tags.join(', ')}`);
|
addDebugInfo(`Generated ${tags.length} tags: ${tags.join(', ')}`);
|
||||||
return tags;
|
return tags;
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
console.warn('Failed to parse tags as JSON, trying to extract from text:', responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try to extract tags from text response
|
|
||||||
const tagMatch = responseText.match(/\[(.*?)\]/);
|
|
||||||
if (tagMatch) {
|
|
||||||
const tags = tagMatch[1]
|
|
||||||
.split(',')
|
|
||||||
.map((tag: string) => tag.trim().replace(/"/g, ''))
|
|
||||||
.filter((tag: string) => tag.length > 0);
|
|
||||||
addDebugInfo(`Extracted ${tags.length} tags from text: ${tags.join(', ')}`);
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: return empty array
|
// Last resort: return empty array
|
||||||
addDebugInfo('Could not extract tags from Ollama response');
|
addDebugInfo('Could not extract tags from Ollama response');
|
||||||
@@ -311,13 +300,34 @@ ${content}`;
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating tags:', error);
|
console.error('Error generating tags:', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
addDebugInfo(`Tag generation failed: ${errorMessage}`);
|
|
||||||
|
|
||||||
// Check if it's a connection error
|
// More detailed error logging and user notification
|
||||||
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('ECONNREFUSED')) {
|
let userMessage = 'Failed to generate tags';
|
||||||
addDebugInfo('Ollama appears to be offline. Please ensure Ollama is running on localhost:11434');
|
|
||||||
|
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||||
|
addDebugInfo('Tag generation failed: Network error - Ollama not reachable at localhost:11434');
|
||||||
|
userMessage = 'Ollama not reachable - check if it\'s running on localhost:11434';
|
||||||
|
} else if (errorMessage.includes('Failed to fetch')) {
|
||||||
|
addDebugInfo('Tag generation failed: Ollama connection refused - check if Ollama is running');
|
||||||
|
userMessage = 'Ollama connection refused - check if Ollama is running';
|
||||||
|
} else if (errorMessage.includes('404')) {
|
||||||
|
addDebugInfo('Tag generation failed: Ollama endpoint not found - check model name');
|
||||||
|
userMessage = 'Model not found - check if llama3.2:3b is installed';
|
||||||
|
} else if (errorMessage.includes('500')) {
|
||||||
|
addDebugInfo('Tag generation failed: Ollama server error - check model is loaded');
|
||||||
|
userMessage = 'Ollama server error - check if model is loaded';
|
||||||
|
} else {
|
||||||
|
addDebugInfo(`Tag generation failed: ${errorMessage}`);
|
||||||
|
userMessage = `Tag generation failed: ${errorMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error to user
|
||||||
|
toast({
|
||||||
|
title: "Tag Generation Failed",
|
||||||
|
description: userMessage,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -434,9 +444,9 @@ ${content}`;
|
|||||||
const { mostFrequentLetter, mostFrequentLetterFrequency, letterCount } = calculateLetterFrequency(trimmedContent);
|
const { mostFrequentLetter, mostFrequentLetterFrequency, letterCount } = calculateLetterFrequency(trimmedContent);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Generate tags using Ollama if enabled
|
// Use current note tags or generate tags using Ollama if enabled
|
||||||
let tags: string[] = [];
|
let tags: string[] = currentNoteTags;
|
||||||
if (autoGenerateTags) {
|
if (autoGenerateTags && tags.length === 0) {
|
||||||
addDebugInfo('Generating tags for new note...');
|
addDebugInfo('Generating tags for new note...');
|
||||||
tags = await generateTags(trimmedContent);
|
tags = await generateTags(trimmedContent);
|
||||||
}
|
}
|
||||||
@@ -480,6 +490,7 @@ ${content}`;
|
|||||||
setNoteCache(prev => [newNote, ...prev]);
|
setNoteCache(prev => [newNote, ...prev]);
|
||||||
setPreviousNote(newNote);
|
setPreviousNote(newNote);
|
||||||
setCurrentNoteIndex(0);
|
setCurrentNoteIndex(0);
|
||||||
|
setCurrentNoteTags([]);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Note saved",
|
title: "Note saved",
|
||||||
@@ -1696,12 +1707,26 @@ ${content}`;
|
|||||||
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 bg-card rounded-lg border border-border shadow-sm p-6 overflow-auto cursor-pointer select-none"
|
||||||
>
|
>
|
||||||
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex justify-between items-center`}>
|
<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})`}
|
||||||
<span className={`ml-2 ${getTextClass('xl')} text-muted-foreground`}>Scroll to navigate</span>
|
<span className={`ml-2 ${getTextClass('xl')} text-muted-foreground`}>Scroll to navigate</span>
|
||||||
</div>
|
</div>
|
||||||
{previousNote && !autoGenerateTags && (
|
{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 tags = e.target.value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
|
||||||
|
const updatedNote = { ...previousNote, tags };
|
||||||
|
setPreviousNote(updatedNote);
|
||||||
|
setIsPreviousNoteModified(true);
|
||||||
|
}}
|
||||||
|
className={`flex-1 border-0 bg-transparent ${getTextClass('base')} p-0 outline-none`}
|
||||||
|
/>
|
||||||
|
{!autoGenerateTags && (
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (previousNote) {
|
if (previousNote) {
|
||||||
@@ -1714,26 +1739,19 @@ ${content}`;
|
|||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`${getTextClass('base')} px-2 py-1`}
|
className={`${getTextClass('base')} px-2 py-1 h-6`}
|
||||||
>
|
>
|
||||||
Generate Tags
|
AI
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{previousNote ? (
|
{previousNote ? (
|
||||||
<div className="space-y-4 h-[calc(100%-2rem)]">
|
<div className="space-y-4 h-[calc(100%-2rem)]">
|
||||||
<div className={`${getTextClass('xl')} text-muted-foreground`}>
|
<div className={`${getTextClass('xl')} text-muted-foreground`}>
|
||||||
{formatDate(previousNote.epochTime)}
|
{formatDate(previousNote.epochTime)}
|
||||||
</div>
|
</div>
|
||||||
{previousNote.tags && previousNote.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{previousNote.tags.map((tag, index) => (
|
|
||||||
<Badge key={index} variant="secondary" className={`${getTextClass('base')} px-2 py-1`}>
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Textarea
|
<Textarea
|
||||||
value={previousNote.content}
|
value={previousNote.content}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1745,7 +1763,7 @@ ${content}`;
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={handlePreviousNoteBlur}
|
onBlur={handlePreviousNoteBlur}
|
||||||
className={`h-[calc(100%-${previousNote.tags && previousNote.tags.length > 0 ? '6rem' : '3rem'})] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
|
className={`h-[calc(100%-3rem)] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
|
||||||
placeholder="Previous entry content..."
|
placeholder="Previous entry content..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1758,7 +1776,37 @@ ${content}`;
|
|||||||
|
|
||||||
{/* 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`}>Current Entry</div>
|
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex items-center gap-4`}>
|
||||||
|
<div>Current Entry</div>
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
<span className={`${getTextClass('base')} text-muted-foreground`}>Tags:</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentNoteTags.join(', ')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const tags = e.target.value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
|
||||||
|
setCurrentNoteTags(tags);
|
||||||
|
}}
|
||||||
|
className={`flex-1 border-0 bg-transparent ${getTextClass('base')} p-0 outline-none`}
|
||||||
|
/>
|
||||||
|
{!autoGenerateTags && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (currentNote.trim()) {
|
||||||
|
addDebugInfo('Manually generating tags for current note...');
|
||||||
|
const tags = await generateTags(currentNote);
|
||||||
|
setCurrentNoteTags(tags);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={`${getTextClass('base')} px-2 py-1 h-6`}
|
||||||
|
>
|
||||||
|
AI
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={currentNoteRef}
|
ref={currentNoteRef}
|
||||||
value={currentNote}
|
value={currentNote}
|
||||||
@@ -1812,12 +1860,8 @@ ${content}`;
|
|||||||
{formatDate(note.epochTime)}
|
{formatDate(note.epochTime)}
|
||||||
</div>
|
</div>
|
||||||
{note.tags && note.tags.length > 0 && (
|
{note.tags && note.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className={`${getTextClass('base')} text-muted-foreground mb-3`}>
|
||||||
{note.tags.map((tag, index) => (
|
Tags: {note.tags.join(', ')}
|
||||||
<Badge key={index} variant="secondary" className={`${getTextClass('base')} px-2 py-1`}>
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
Reference in New Issue
Block a user