Hallucinate tags and automatically generating tags via ollama
Holy shit what a fat change !!!!
This commit is contained in:
@@ -31,6 +31,7 @@ interface Note {
|
|||||||
topLetter: string;
|
topLetter: string;
|
||||||
topLetterFrequency: number;
|
topLetterFrequency: number;
|
||||||
letterCount: number;
|
letterCount: number;
|
||||||
|
tags?: string[];
|
||||||
snippet?: string;
|
snippet?: string;
|
||||||
isProblematic?: boolean;
|
isProblematic?: boolean;
|
||||||
problemReason?: string;
|
problemReason?: string;
|
||||||
@@ -47,6 +48,10 @@ const MEILISEARCH_API_KEY = '31qXgGbQ3lT4DYHQ0TOKpMzh7wcigs7agHyxP5Fz6T6D61xsvNr
|
|||||||
const NOTE_INDEX = 'notes';
|
const NOTE_INDEX = 'notes';
|
||||||
const SCRATCH_INDEX = 'scratch';
|
const SCRATCH_INDEX = 'scratch';
|
||||||
const SETTINGS_INDEX = 'settings';
|
const SETTINGS_INDEX = 'settings';
|
||||||
|
|
||||||
|
// Ollama configuration
|
||||||
|
const OLLAMA_ENDPOINT = 'http://localhost:11434';
|
||||||
|
const OLLAMA_MODEL = 'gemma3:4b-it-qat';
|
||||||
const meiliHeaders = {
|
const meiliHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||||
@@ -60,6 +65,7 @@ const mapHitToNote = (hit: any): Note => ({
|
|||||||
topLetter: hit.topLetter,
|
topLetter: hit.topLetter,
|
||||||
topLetterFrequency: hit.topLetterFrequency,
|
topLetterFrequency: hit.topLetterFrequency,
|
||||||
letterCount: hit.letterCount || 0,
|
letterCount: hit.letterCount || 0,
|
||||||
|
tags: hit.tags || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
@@ -88,6 +94,8 @@ const Index = () => {
|
|||||||
const [cacheMode, setCacheMode] = useState<'global' | 'scoped'>('global');
|
const [cacheMode, setCacheMode] = useState<'global' | 'scoped'>('global');
|
||||||
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
||||||
const [showDebugPanel, setShowDebugPanel] = useState(false);
|
const [showDebugPanel, setShowDebugPanel] = useState(false);
|
||||||
|
const [autoGenerateTags, setAutoGenerateTags] = useState(true);
|
||||||
|
const [ollamaStatus, setOllamaStatus] = useState<'unknown' | 'online' | 'offline'>('unknown');
|
||||||
|
|
||||||
const { resolvedTheme, setTheme } = useTheme();
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
|
||||||
@@ -207,6 +215,113 @@ const Index = () => {
|
|||||||
return { mostFrequentLetter, mostFrequentLetterFrequency, letterCount };
|
return { mostFrequentLetter, mostFrequentLetterFrequency, letterCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check Ollama status
|
||||||
|
const checkOllamaStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${OLLAMA_ENDPOINT}/api/tags`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setOllamaStatus('online');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
setOllamaStatus('offline');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setOllamaStatus('offline');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate tags using Ollama
|
||||||
|
const generateTags = async (content: string): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const systemPrompt = `You are a helpful assistant that generates searchable tags for journal entries.
|
||||||
|
|
||||||
|
Your task is to analyze the content and generate 3-8 relevant tags that would help the author find this note later.
|
||||||
|
Focus on:
|
||||||
|
- Programming languages, frameworks, libraries mentioned
|
||||||
|
- Technical concepts and methodologies
|
||||||
|
- Tools and utilities discussed
|
||||||
|
- 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"]
|
||||||
|
|
||||||
|
Keep tags concise, use lowercase, and separate words with hyphens if needed.`;
|
||||||
|
|
||||||
|
const userPrompt = `Generate tags for this journal entry:
|
||||||
|
|
||||||
|
${content}`;
|
||||||
|
|
||||||
|
const response = await fetchWithTiming(`${OLLAMA_ENDPOINT}/api/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: OLLAMA_MODEL,
|
||||||
|
system: systemPrompt,
|
||||||
|
prompt: userPrompt,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
}, 'Generate Tags');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to generate tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const responseText = data.response?.trim();
|
||||||
|
|
||||||
|
if (!responseText) {
|
||||||
|
throw new Error('Empty response from Ollama');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse JSON from the response
|
||||||
|
try {
|
||||||
|
const tags = JSON.parse(responseText);
|
||||||
|
if (Array.isArray(tags) && tags.every(tag => typeof tag === 'string')) {
|
||||||
|
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');
|
||||||
|
return [];
|
||||||
|
} 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Clean content function
|
// Clean content function
|
||||||
const cleanContent = (content: string) => {
|
const cleanContent = (content: string) => {
|
||||||
return content.trim();
|
return content.trim();
|
||||||
@@ -319,6 +434,13 @@ const Index = () => {
|
|||||||
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
|
||||||
|
let tags: string[] = [];
|
||||||
|
if (autoGenerateTags) {
|
||||||
|
addDebugInfo('Generating tags for new note...');
|
||||||
|
tags = await generateTags(trimmedContent);
|
||||||
|
}
|
||||||
|
|
||||||
const document = {
|
const document = {
|
||||||
id: generateRandomString(32),
|
id: generateRandomString(32),
|
||||||
date: now.getTime(),
|
date: now.getTime(),
|
||||||
@@ -327,6 +449,7 @@ const Index = () => {
|
|||||||
topLetter: mostFrequentLetter,
|
topLetter: mostFrequentLetter,
|
||||||
topLetterFrequency: mostFrequentLetterFrequency,
|
topLetterFrequency: mostFrequentLetterFrequency,
|
||||||
letterCount: letterCount,
|
letterCount: letterCount,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/documents`, {
|
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/documents`, {
|
||||||
@@ -350,6 +473,7 @@ const Index = () => {
|
|||||||
topLetter: document.topLetter,
|
topLetter: document.topLetter,
|
||||||
topLetterFrequency: document.topLetterFrequency,
|
topLetterFrequency: document.topLetterFrequency,
|
||||||
letterCount: document.letterCount,
|
letterCount: document.letterCount,
|
||||||
|
tags: document.tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update cache and set as previous note
|
// Update cache and set as previous note
|
||||||
@@ -359,7 +483,7 @@ const Index = () => {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Note saved",
|
title: "Note saved",
|
||||||
description: "Your note has been saved successfully.",
|
description: `Your note has been saved successfully${tags.length > 0 ? ` with ${tags.length} tags` : ''}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newNote;
|
return newNote;
|
||||||
@@ -380,6 +504,13 @@ const Index = () => {
|
|||||||
const trimmedContent = cleanContent(note.content);
|
const trimmedContent = cleanContent(note.content);
|
||||||
const { mostFrequentLetter, mostFrequentLetterFrequency, letterCount } = calculateLetterFrequency(trimmedContent);
|
const { mostFrequentLetter, mostFrequentLetterFrequency, letterCount } = calculateLetterFrequency(trimmedContent);
|
||||||
|
|
||||||
|
// Regenerate tags if content has changed significantly and auto-generation is enabled
|
||||||
|
let tags = note.tags || [];
|
||||||
|
if (autoGenerateTags && trimmedContent !== note.content) {
|
||||||
|
addDebugInfo('Content changed, regenerating tags...');
|
||||||
|
tags = await generateTags(trimmedContent);
|
||||||
|
}
|
||||||
|
|
||||||
const document = {
|
const document = {
|
||||||
id: note.id,
|
id: note.id,
|
||||||
content: trimmedContent,
|
content: trimmedContent,
|
||||||
@@ -388,6 +519,7 @@ const Index = () => {
|
|||||||
topLetter: mostFrequentLetter,
|
topLetter: mostFrequentLetter,
|
||||||
topLetterFrequency: mostFrequentLetterFrequency,
|
topLetterFrequency: mostFrequentLetterFrequency,
|
||||||
letterCount: letterCount,
|
letterCount: letterCount,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/documents`, {
|
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/documents`, {
|
||||||
@@ -404,11 +536,11 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
setNoteCache(prev => prev.map(n => n.id === note.id ? { ...note, content: trimmedContent, letterCount } : n));
|
setNoteCache(prev => prev.map(n => n.id === note.id ? { ...note, content: trimmedContent, letterCount, tags } : n));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Note updated",
|
title: "Note updated",
|
||||||
description: "Your changes have been saved.",
|
description: `Your changes have been saved${tags.length > 0 ? ` with ${tags.length} tags` : ''}.`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating note:', error);
|
console.error('Error updating note:', error);
|
||||||
@@ -637,7 +769,7 @@ const Index = () => {
|
|||||||
q: query,
|
q: query,
|
||||||
matchingStrategy: 'all',
|
matchingStrategy: 'all',
|
||||||
limit: 50,
|
limit: 50,
|
||||||
attributesToHighlight: ['content'],
|
attributesToHighlight: ['content', 'tags'],
|
||||||
showRankingScore: true,
|
showRankingScore: true,
|
||||||
highlightPreTag: '<mark class="bg-yellow-200">',
|
highlightPreTag: '<mark class="bg-yellow-200">',
|
||||||
highlightPostTag: '</mark>',
|
highlightPostTag: '</mark>',
|
||||||
@@ -863,6 +995,65 @@ const Index = () => {
|
|||||||
setIsCleanupOpen(true);
|
setIsCleanupOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Regenerate tags for all notes
|
||||||
|
const regenerateAllTags = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
addDebugInfo('Regenerating tags for all notes...');
|
||||||
|
|
||||||
|
// Load all notes
|
||||||
|
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
q: '',
|
||||||
|
limit: 1000,
|
||||||
|
sort: ['date:desc'],
|
||||||
|
}),
|
||||||
|
}, 'Load All Notes for Tag Regeneration');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load notes for tag regeneration');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const notes: Note[] = data.hits.map((hit: any) => mapHitToNote(hit));
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
for (const note of notes) {
|
||||||
|
try {
|
||||||
|
const newTags = await generateTags(note.content);
|
||||||
|
if (JSON.stringify(newTags) !== JSON.stringify(note.tags || [])) {
|
||||||
|
const updatedNote = { ...note, tags: newTags };
|
||||||
|
await updateNote(updatedNote);
|
||||||
|
updatedCount++;
|
||||||
|
addDebugInfo(`Updated tags for note ${note.id}: ${newTags.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating tags for note ${note.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDebugInfo(`Tag regeneration complete: ${updatedCount} notes updated`);
|
||||||
|
toast({
|
||||||
|
title: "Tag regeneration complete",
|
||||||
|
description: `Updated tags for ${updatedCount} notes.`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error regenerating tags:', error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to regenerate tags. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update the keyboard shortcuts effect
|
// Update the keyboard shortcuts effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||||
@@ -1154,7 +1345,7 @@ const Index = () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(['date', 'topLetter', 'letterCount', 'topLetterFrequency']),
|
body: JSON.stringify(['date', 'topLetter', 'letterCount', 'topLetterFrequency', 'tags']),
|
||||||
}, 'Configure Notes Filterable Attributes'),
|
}, 'Configure Notes Filterable Attributes'),
|
||||||
|
|
||||||
// Scratch index configurations
|
// Scratch index configurations
|
||||||
@@ -1247,6 +1438,8 @@ const Index = () => {
|
|||||||
await loadNotes();
|
await loadNotes();
|
||||||
await loadLatestScratch();
|
await loadLatestScratch();
|
||||||
await loadFontSizeSetting();
|
await loadFontSizeSetting();
|
||||||
|
await loadAutoGenerateTagsSetting();
|
||||||
|
await checkOllamaStatus();
|
||||||
|
|
||||||
const totalTime = Date.now() - dataLoadStartTime;
|
const totalTime = Date.now() - dataLoadStartTime;
|
||||||
debugTiming('Total Data Loading', dataLoadStartTime);
|
debugTiming('Total Data Loading', dataLoadStartTime);
|
||||||
@@ -1302,6 +1495,33 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load auto-generate tags setting
|
||||||
|
const loadAutoGenerateTagsSetting = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${SETTINGS_INDEX}/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
q: 'autoGenerateTags',
|
||||||
|
filter: 'key = "autoGenerateTags"',
|
||||||
|
limit: 1,
|
||||||
|
}),
|
||||||
|
}, 'Load Auto Generate Tags Setting');
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.hits.length > 0) {
|
||||||
|
setAutoGenerateTags(data.hits[0].value === 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading auto-generate tags setting:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Save font size setting
|
// Save font size setting
|
||||||
const saveFontSizeSetting = async (newFontSize: string) => {
|
const saveFontSizeSetting = async (newFontSize: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -1339,6 +1559,43 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save auto-generate tags setting
|
||||||
|
const saveAutoGenerateTagsSetting = async (enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
const document = {
|
||||||
|
key: 'autoGenerateTags',
|
||||||
|
value: enabled.toString(),
|
||||||
|
updatedAt: new Date().getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetchWithTiming(`${MEILISEARCH_ENDPOINT}/indexes/${SETTINGS_INDEX}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(document),
|
||||||
|
}, 'Save Auto Generate Tags Setting');
|
||||||
|
|
||||||
|
if (response.status !== 202) {
|
||||||
|
throw new Error('Failed to save auto-generate tags setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoGenerateTags(enabled);
|
||||||
|
toast({
|
||||||
|
title: "Auto-tagging updated",
|
||||||
|
description: `Automatic tag generation ${enabled ? 'enabled' : 'disabled'}.`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-generate tags setting:', error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to save auto-generate tags setting.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen bg-background text-foreground flex flex-col ${getTextClass('base')}`}>
|
<div className={`min-h-screen bg-background text-foreground flex flex-col ${getTextClass('base')}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -1366,6 +1623,14 @@ const Index = () => {
|
|||||||
<SelectItem value="xl">Extra Large</SelectItem>
|
<SelectItem value="xl">Extra Large</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${getTextClass('base')} text-muted-foreground`}>Auto-tags</span>
|
||||||
|
<Switch
|
||||||
|
checked={autoGenerateTags}
|
||||||
|
onCheckedChange={saveAutoGenerateTagsSetting}
|
||||||
|
aria-label="Toggle automatic tag generation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button onClick={handleCleanup} size="sm" variant="outline" className={getTextClass('base')}>
|
<Button onClick={handleCleanup} size="sm" variant="outline" className={getTextClass('base')}>
|
||||||
<Trash2 className="h-8 w-8" />
|
<Trash2 className="h-8 w-8" />
|
||||||
Cleanup
|
Cleanup
|
||||||
@@ -1386,6 +1651,26 @@ const Index = () => {
|
|||||||
{showDebugPanel && (
|
{showDebugPanel && (
|
||||||
<div className="bg-muted border-b border-border p-4 max-h-48 overflow-y-auto">
|
<div className="bg-muted border-b border-border p-4 max-h-48 overflow-y-auto">
|
||||||
<div className={`${getTextClass('xl')} font-semibold mb-2`}>Debug Information</div>
|
<div className={`${getTextClass('xl')} font-semibold mb-2`}>Debug Information</div>
|
||||||
|
<div className="flex items-center gap-4 mb-3">
|
||||||
|
<div className={`${getTextClass('base')} flex items-center gap-2`}>
|
||||||
|
<span>Ollama Status:</span>
|
||||||
|
<Badge
|
||||||
|
variant={ollamaStatus === 'online' ? 'default' : ollamaStatus === 'offline' ? 'destructive' : 'secondary'}
|
||||||
|
className={`${getTextClass('base')} px-2 py-1`}
|
||||||
|
>
|
||||||
|
{ollamaStatus === 'online' ? 'Online' : ollamaStatus === 'offline' ? 'Offline' : 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className={`${getTextClass('base')} flex items-center gap-2`}>
|
||||||
|
<span>Auto-tags:</span>
|
||||||
|
<Badge
|
||||||
|
variant={autoGenerateTags ? 'default' : 'secondary'}
|
||||||
|
className={`${getTextClass('base')} px-2 py-1`}
|
||||||
|
>
|
||||||
|
{autoGenerateTags ? 'Enabled' : 'Disabled'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{debugInfo.length > 0 ? (
|
{debugInfo.length > 0 ? (
|
||||||
debugInfo.map((info, index) => (
|
debugInfo.map((info, index) => (
|
||||||
@@ -1411,15 +1696,44 @@ const Index = () => {
|
|||||||
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`}>
|
<div className={`${getTextClass('2xl')} text-muted-foreground mb-3 flex justify-between items-center`}>
|
||||||
Previous Entry {currentNoteIndex > 0 && `(${currentNoteIndex + 1} of ${noteCache.length})`}
|
<div>
|
||||||
<span className={`ml-2 ${getTextClass('xl')} text-muted-foreground`}>Scroll to navigate</span>
|
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 && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (previousNote) {
|
||||||
|
addDebugInfo('Manually generating tags...');
|
||||||
|
const tags = await generateTags(previousNote.content);
|
||||||
|
const updatedNote = { ...previousNote, tags };
|
||||||
|
setPreviousNote(updatedNote);
|
||||||
|
setIsPreviousNoteModified(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={`${getTextClass('base')} px-2 py-1`}
|
||||||
|
>
|
||||||
|
Generate Tags
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</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) => {
|
||||||
@@ -1431,7 +1745,7 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={handlePreviousNoteBlur}
|
onBlur={handlePreviousNoteBlur}
|
||||||
className={`h-[calc(100%-3rem)] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
|
className={`h-[calc(100%-${previousNote.tags && previousNote.tags.length > 0 ? '6rem' : '3rem'})] border-0 resize-none focus:ring-0 bg-transparent ${getTextClass('2xl')}`}
|
||||||
placeholder="Previous entry content..."
|
placeholder="Previous entry content..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1497,6 +1811,15 @@ const Index = () => {
|
|||||||
<div className={`${getTextClass('xl')} text-muted-foreground mb-2`}>
|
<div className={`${getTextClass('xl')} text-muted-foreground mb-2`}>
|
||||||
{formatDate(note.epochTime)}
|
{formatDate(note.epochTime)}
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`${getTextClass('2xl')} whitespace-pre-wrap`}
|
className={`${getTextClass('2xl')} whitespace-pre-wrap`}
|
||||||
dangerouslySetInnerHTML={{ __html: note.snippet || note.content }}
|
dangerouslySetInnerHTML={{ __html: note.snippet || note.content }}
|
||||||
@@ -1598,9 +1921,19 @@ const Index = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={getProblematicNotes} className={`w-full ${getTextClass('2xl')} py-6`}>
|
<div className="space-y-4">
|
||||||
Analyze Notes
|
<Button onClick={getProblematicNotes} className={`w-full ${getTextClass('2xl')} py-6`}>
|
||||||
</Button>
|
Analyze Notes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={regenerateAllTags}
|
||||||
|
variant="outline"
|
||||||
|
className={`w-full ${getTextClass('2xl')} py-6`}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Regenerating Tags...' : 'Regenerate All Tags'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-auto max-h-[50vh] space-y-6">
|
<div className="overflow-auto max-h-[50vh] space-y-6">
|
||||||
|
Reference in New Issue
Block a user