feat(Index.tsx): add settings modal for tag generation and display preferences
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Trash2, Moon, Sun } from 'lucide-react';
|
import { Trash2, Moon, Sun, Settings } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
@@ -99,6 +99,23 @@ const Index = () => {
|
|||||||
const [ollamaStatus, setOllamaStatus] = useState<'unknown' | 'online' | 'offline'>('unknown');
|
const [ollamaStatus, setOllamaStatus] = useState<'unknown' | 'online' | 'offline'>('unknown');
|
||||||
const [includeTagsInSearch, setIncludeTagsInSearch] = useState(true);
|
const [includeTagsInSearch, setIncludeTagsInSearch] = useState(true);
|
||||||
const [tagGenerationTimeout, setTagGenerationTimeout] = useState<NodeJS.Timeout>();
|
const [tagGenerationTimeout, setTagGenerationTimeout] = useState<NodeJS.Timeout>();
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
const [systemPrompt, setSystemPrompt] = useState(`You are a helpful assistant that generates searchable tags for journal entries.
|
||||||
|
|
||||||
|
Your task is to analyze the content and generate 1-3 relevant tags that would help the author find this note later.
|
||||||
|
Focus on the quality of tags, not the quantity.
|
||||||
|
|
||||||
|
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 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.`);
|
||||||
|
const [contextSize, setContextSize] = useState(3);
|
||||||
|
|
||||||
const { resolvedTheme, setTheme } = useTheme();
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
|
||||||
@@ -276,28 +293,12 @@ const Index = () => {
|
|||||||
// 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 {
|
||||||
const systemPrompt = `You are a helpful assistant that generates searchable tags for journal entries.
|
|
||||||
|
|
||||||
Your task is to analyze the content and generate 1-3 relevant tags that would help the author find this note later.
|
|
||||||
Focus on the quality of tags, not the quantity.
|
|
||||||
|
|
||||||
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 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.`;
|
|
||||||
|
|
||||||
// Get context from surrounding notes
|
// Get context from surrounding notes
|
||||||
let context = '';
|
let context = '';
|
||||||
if (noteIndex !== undefined && noteCache.length > 0) {
|
if (noteIndex !== undefined && noteCache.length > 0) {
|
||||||
const contextNotes = [];
|
const contextNotes = [];
|
||||||
const start = Math.max(0, noteIndex - 2);
|
const start = Math.max(0, noteIndex - contextSize);
|
||||||
const end = Math.min(noteCache.length, noteIndex + 3);
|
const end = Math.min(noteCache.length, noteIndex + contextSize + 1);
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
if (i !== noteIndex) {
|
if (i !== noteIndex) {
|
||||||
@@ -1519,6 +1520,8 @@ ${content}${context}`;
|
|||||||
await loadLatestScratch();
|
await loadLatestScratch();
|
||||||
await loadFontSizeSetting();
|
await loadFontSizeSetting();
|
||||||
await loadAutoGenerateTagsSetting();
|
await loadAutoGenerateTagsSetting();
|
||||||
|
await loadSystemPromptSetting();
|
||||||
|
await loadContextSizeSetting();
|
||||||
await checkOllamaStatus();
|
await checkOllamaStatus();
|
||||||
|
|
||||||
const totalTime = Date.now() - dataLoadStartTime;
|
const totalTime = Date.now() - dataLoadStartTime;
|
||||||
@@ -1602,6 +1605,60 @@ ${content}${context}`;
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load system prompt setting
|
||||||
|
const loadSystemPromptSetting = 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: 'systemPrompt',
|
||||||
|
filter: 'key = "systemPrompt"',
|
||||||
|
limit: 1,
|
||||||
|
}),
|
||||||
|
}, 'Load System Prompt Setting');
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.hits.length > 0) {
|
||||||
|
setSystemPrompt(data.hits[0].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading system prompt setting:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load context size setting
|
||||||
|
const loadContextSizeSetting = 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: 'contextSize',
|
||||||
|
filter: 'key = "contextSize"',
|
||||||
|
limit: 1,
|
||||||
|
}),
|
||||||
|
}, 'Load Context Size Setting');
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.hits.length > 0) {
|
||||||
|
setContextSize(Number(data.hits[0].value) || 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading context size setting:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Save font size setting
|
// Save font size setting
|
||||||
const saveFontSizeSetting = async (newFontSize: string) => {
|
const saveFontSizeSetting = async (newFontSize: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -1676,6 +1733,80 @@ ${content}${context}`;
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save system prompt setting
|
||||||
|
const saveSystemPromptSetting = async (newPrompt: string) => {
|
||||||
|
try {
|
||||||
|
const document = {
|
||||||
|
key: 'systemPrompt',
|
||||||
|
value: newPrompt,
|
||||||
|
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 System Prompt Setting');
|
||||||
|
|
||||||
|
if (response.status !== 202) {
|
||||||
|
throw new Error('Failed to save system prompt setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSystemPrompt(newPrompt);
|
||||||
|
toast({
|
||||||
|
title: "System prompt updated",
|
||||||
|
description: "Tag generation prompt has been saved.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving system prompt setting:', error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to save system prompt setting.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save context size setting
|
||||||
|
const saveContextSizeSetting = async (newSize: number) => {
|
||||||
|
try {
|
||||||
|
const document = {
|
||||||
|
key: 'contextSize',
|
||||||
|
value: newSize.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 Context Size Setting');
|
||||||
|
|
||||||
|
if (response.status !== 202) {
|
||||||
|
throw new Error('Failed to save context size setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
setContextSize(newSize);
|
||||||
|
toast({
|
||||||
|
title: "Context size updated",
|
||||||
|
description: `Context size changed to ${newSize} notes.`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving context size setting:', error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to save context size 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 */}
|
||||||
@@ -1715,6 +1846,15 @@ ${content}${context}`;
|
|||||||
<Trash2 className="h-8 w-8" />
|
<Trash2 className="h-8 w-8" />
|
||||||
Cleanup
|
Cleanup
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={getTextClass('base')}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowDebugPanel(!showDebugPanel)}
|
onClick={() => setShowDebugPanel(!showDebugPanel)}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -2101,6 +2241,136 @@ ${content}${context}`;
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Settings Modal */}
|
||||||
|
<Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden bg-popover border-border">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className={`${getTextClass('4xl')}`}>Settings</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-8 overflow-auto max-h-[70vh]">
|
||||||
|
{/* Tag Generation Settings */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className={`${getTextClass('2xl')} font-semibold`}>Tag Generation</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('xl')} font-medium`}>Automatic Tag Generation</label>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground`}>
|
||||||
|
Automatically generate tags while typing and before saving
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={autoGenerateTags}
|
||||||
|
onCheckedChange={saveAutoGenerateTagsSetting}
|
||||||
|
aria-label="Toggle automatic tag generation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('xl')} font-medium mb-2 block`}>
|
||||||
|
Context Size: {contextSize} notes
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
value={[contextSize]}
|
||||||
|
onValueChange={(value) => saveContextSizeSetting(value[0])}
|
||||||
|
max={10}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
className="w-full h-8"
|
||||||
|
/>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground mt-2`}>
|
||||||
|
Number of surrounding notes to include as context for tag generation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('xl')} font-medium mb-2 block`}>
|
||||||
|
System Prompt
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={systemPrompt}
|
||||||
|
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||||
|
onBlur={() => saveSystemPromptSetting(systemPrompt)}
|
||||||
|
className={`min-h-32 ${getTextClass('base')}`}
|
||||||
|
placeholder="Enter the system prompt for tag generation..."
|
||||||
|
/>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground mt-2`}>
|
||||||
|
This prompt tells the AI how to generate tags for your notes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Settings */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className={`${getTextClass('2xl')} font-semibold`}>Search</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('xl')} font-medium`}>Include Tags in Search</label>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground`}>
|
||||||
|
Search both content and tags when enabled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={includeTagsInSearch}
|
||||||
|
onCheckedChange={setIncludeTagsInSearch}
|
||||||
|
aria-label="Toggle tag search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Display Settings */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className={`${getTextClass('2xl')} font-semibold`}>Display</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('xl')} font-medium mb-2 block`}>Font Size</label>
|
||||||
|
<Select value={fontSize} onValueChange={saveFontSizeSetting}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="Font Size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="small">Small</SelectItem>
|
||||||
|
<SelectItem value="medium">Medium</SelectItem>
|
||||||
|
<SelectItem value="large">Large</SelectItem>
|
||||||
|
<SelectItem value="xl">Extra Large</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ollama Settings */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className={`${getTextClass('2xl')} font-semibold`}>Ollama Configuration</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${getTextClass('base')}`}>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>
|
||||||
|
<label className={`${getTextClass('base')} font-medium`}>Endpoint:</label>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground`}>{OLLAMA_ENDPOINT}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`${getTextClass('base')} font-medium`}>Model:</label>
|
||||||
|
<p className={`${getTextClass('base')} text-muted-foreground`}>{OLLAMA_MODEL}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user