feat(Index.tsx): refactor tag generation and display for notes to use comma-separated values and allow manual editing

This commit is contained in:
2025-08-29 10:57:10 +02:00
parent 862a4f2e3b
commit b25103f74b

View File

@@ -73,6 +73,7 @@ const Index = () => {
console.log(`Component mounted after ${Date.now() - GLOBAL_START_TIME}ms`);
const [currentNote, setCurrentNote] = useState('');
const [currentNoteTags, setCurrentNoteTags] = useState<string[]>([]);
const [previousNote, setPreviousNote] = useState<Note | null>(null);
const [scratchPad, setScratchPad] = useState('');
const [scratchId, setScratchId] = useState<string | null>(null);
@@ -251,7 +252,7 @@ Focus on:
- Problem domains or contexts
- 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.`;
@@ -273,37 +274,25 @@ ${content}`;
}, 'Generate Tags');
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 responseText = data.response?.trim();
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
try {
const tags = JSON.parse(responseText);
if (Array.isArray(tags) && tags.every(tag => typeof tag === 'string')) {
// Parse CSV format from the response
const tags = responseText
.split(',')
.map((tag: string) => tag.trim())
.filter((tag: string) => tag.length > 0);
addDebugInfo(`Generated ${tags.length} tags: ${tags.join(', ')}`);
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
addDebugInfo('Could not extract tags from Ollama response');
@@ -311,13 +300,34 @@ ${content}`;
} catch (error) {
console.error('Error generating tags:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
addDebugInfo(`Tag generation failed: ${errorMessage}`);
// Check if it's a connection error
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('ECONNREFUSED')) {
addDebugInfo('Ollama appears to be offline. Please ensure Ollama is running on localhost:11434');
// More detailed error logging and user notification
let userMessage = 'Failed to generate tags';
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 [];
}
};
@@ -434,9 +444,9 @@ ${content}`;
const { mostFrequentLetter, mostFrequentLetterFrequency, letterCount } = calculateLetterFrequency(trimmedContent);
const now = new Date();
// Generate tags using Ollama if enabled
let tags: string[] = [];
if (autoGenerateTags) {
// Use current note tags or generate tags using Ollama if enabled
let tags: string[] = currentNoteTags;
if (autoGenerateTags && tags.length === 0) {
addDebugInfo('Generating tags for new note...');
tags = await generateTags(trimmedContent);
}
@@ -480,6 +490,7 @@ ${content}`;
setNoteCache(prev => [newNote, ...prev]);
setPreviousNote(newNote);
setCurrentNoteIndex(0);
setCurrentNoteTags([]);
toast({
title: "Note saved",
@@ -1696,12 +1707,26 @@ ${content}`;
ref={previousNoteRef}
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>
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`}
<span className={`ml-2 ${getTextClass('xl')} text-muted-foreground`}>Scroll to navigate</span>
</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
onClick={async () => {
if (previousNote) {
@@ -1714,26 +1739,19 @@ ${content}`;
}}
size="sm"
variant="outline"
className={`${getTextClass('base')} px-2 py-1`}
className={`${getTextClass('base')} px-2 py-1 h-6`}
>
Generate Tags
AI
</Button>
)}
</div>
)}
</div>
{previousNote ? (
<div className="space-y-4 h-[calc(100%-2rem)]">
<div className={`${getTextClass('xl')} text-muted-foreground`}>
{formatDate(previousNote.epochTime)}
</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
value={previousNote.content}
onChange={(e) => {
@@ -1745,7 +1763,7 @@ ${content}`;
}
}}
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..."
/>
</div>
@@ -1758,7 +1776,37 @@ ${content}`;
{/* Current Note - Bottom Half */}
<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
ref={currentNoteRef}
value={currentNote}
@@ -1812,12 +1860,8 @@ ${content}`;
{formatDate(note.epochTime)}
</div>
{note.tags && note.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{note.tags.map((tag, index) => (
<Badge key={index} variant="secondary" className={`${getTextClass('base')} px-2 py-1`}>
{tag}
</Badge>
))}
<div className={`${getTextClass('base')} text-muted-foreground mb-3`}>
Tags: {note.tags.join(', ')}
</div>
)}
<div