WIP: Améliorations UX et corrections de bugs avant création des épiques

This commit is contained in:
2026-01-17 11:10:50 +01:00
parent 772dc77719
commit ef60dafd73
84 changed files with 11846 additions and 230 deletions

View File

@@ -3,7 +3,8 @@
import { useState, useEffect, useCallback } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { Note } from '@/lib/types'
import { getAllNotes, searchNotes } from '@/app/actions/notes'
import { getAllNotes, getPinnedNotes, getRecentNotes, searchNotes } from '@/app/actions/notes'
import { getAISettings } from '@/app/actions/ai-settings'
import { NoteInput } from '@/components/note-input'
import { MasonryGrid } from '@/components/masonry-grid'
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
@@ -11,6 +12,8 @@ import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
import { NoteEditor } from '@/components/note-editor'
import { BatchOrganizationDialog } from '@/components/batch-organization-dialog'
import { AutoLabelSuggestionDialog } from '@/components/auto-label-suggestion-dialog'
import { FavoritesSection } from '@/components/favorites-section'
import { RecentNotesSection } from '@/components/recent-notes-section'
import { Button } from '@/components/ui/button'
import { Wand2 } from 'lucide-react'
import { useLabels } from '@/context/LabelContext'
@@ -23,6 +26,9 @@ export default function HomePage() {
const searchParams = useSearchParams()
const router = useRouter()
const [notes, setNotes] = useState<Note[]>([])
const [pinnedNotes, setPinnedNotes] = useState<Note[]>([])
const [recentNotes, setRecentNotes] = useState<Note[]>([])
const [showRecentNotes, setShowRecentNotes] = useState(false)
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [notebookSuggestion, setNotebookSuggestion] = useState<{ noteId: string; content: string } | null>(null)
@@ -45,10 +51,67 @@ export default function HomePage() {
const notebookFilter = searchParams.get('notebook')
const isInbox = !notebookFilter
// Callback for NoteInput to trigger notebook suggestion
// Callback for NoteInput to trigger notebook suggestion and update UI
const handleNoteCreated = useCallback((note: Note) => {
console.log('[NotebookSuggestion] Note created:', { id: note.id, notebookId: note.notebookId, contentLength: note.content?.length })
// Update UI immediately by adding the note to the list if it matches current filters
setNotes((prevNotes) => {
// Check if note matches current filters
const notebookFilter = searchParams.get('notebook')
const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || []
const colorFilter = searchParams.get('color')
const search = searchParams.get('search')?.trim() || null
// Check notebook filter
if (notebookFilter && note.notebookId !== notebookFilter) {
return prevNotes // Note doesn't match notebook filter
}
if (!notebookFilter && note.notebookId) {
return prevNotes // Viewing inbox but note has notebook
}
// Check label filter
if (labelFilter.length > 0) {
const noteLabels = note.labels || []
if (!noteLabels.some((label: string) => labelFilter.includes(label))) {
return prevNotes // Note doesn't match label filter
}
}
// Check color filter
if (colorFilter) {
const labelNamesWithColor = labels
.filter((label: any) => label.color === colorFilter)
.map((label: any) => label.name)
const noteLabels = note.labels || []
if (!noteLabels.some((label: string) => labelNamesWithColor.includes(label))) {
return prevNotes // Note doesn't match color filter
}
}
// Check search filter (simple check - if searching, let refresh handle it)
if (search) {
// If searching, refresh to get proper search results
router.refresh()
return prevNotes
}
// Note matches all filters - add it optimistically to the beginning of the list
// (newest notes first based on order: isPinned desc, order asc, updatedAt desc)
const isPinned = note.isPinned || false
const pinnedNotes = prevNotes.filter(n => n.isPinned)
const unpinnedNotes = prevNotes.filter(n => !n.isPinned)
if (isPinned) {
// Add to beginning of pinned notes
return [note, ...pinnedNotes, ...unpinnedNotes]
} else {
// Add to beginning of unpinned notes
return [...pinnedNotes, note, ...unpinnedNotes]
}
})
// Only suggest if note has no notebook and has 20+ words
if (!note.notebookId) {
const wordCount = (note.content || '').trim().split(/\s+/).filter(w => w.length > 0).length
@@ -66,7 +129,10 @@ export default function HomePage() {
} else {
console.log('[NotebookSuggestion] Note has notebook, skipping')
}
}, [])
// Refresh in background to ensure data consistency (non-blocking)
router.refresh()
}, [searchParams, labels, router])
const handleOpenNote = (noteId: string) => {
const note = notes.find(n => n.id === noteId)
@@ -78,6 +144,19 @@ export default function HomePage() {
// Enable reminder notifications
useReminderCheck(notes)
// Load user settings to check if recent notes should be shown
useEffect(() => {
const loadSettings = async () => {
try {
const settings = await getAISettings()
setShowRecentNotes(settings.showRecentNotes === true)
} catch (error) {
setShowRecentNotes(false)
}
}
loadSettings()
}, [refreshKey])
useEffect(() => {
const loadNotes = async () => {
setIsLoading(true)
@@ -117,13 +196,37 @@ export default function HomePage() {
)
}
// Load pinned notes separately (shown in favorites section)
const pinned = await getPinnedNotes()
// Filter pinned notes by current filters as well
if (notebookFilter) {
setPinnedNotes(pinned.filter((note: any) => note.notebookId === notebookFilter))
} else {
// If no notebook selected, only show pinned notes without notebook
setPinnedNotes(pinned.filter((note: any) => !note.notebookId))
}
// Load recent notes only if enabled in settings
if (showRecentNotes) {
const recent = await getRecentNotes(3)
// Filter recent notes by current filters
if (notebookFilter) {
setRecentNotes(recent.filter((note: any) => note.notebookId === notebookFilter))
} else {
setRecentNotes(recent.filter((note: any) => !note.notebookId))
}
} else {
setRecentNotes([])
}
setNotes(allNotes)
setIsLoading(false)
}
loadNotes()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, refreshKey]) // Intentionally omit 'labels' and 'semantic' to prevent reload when adding tags or from router.push
}, [searchParams, refreshKey, showRecentNotes]) // Intentionally omit 'labels' and 'semantic' to prevent reload when adding tags or from router.push
return (
<main className="container mx-auto px-4 py-8 max-w-7xl">
<NoteInput onNoteCreated={handleNoteCreated} />
@@ -145,10 +248,38 @@ export default function HomePage() {
{isLoading ? (
<div className="text-center py-8 text-gray-500">Loading...</div>
) : (
<MasonryGrid
notes={notes}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
<>
{/* Favorites Section - Pinned Notes */}
<FavoritesSection
pinnedNotes={pinnedNotes}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
{/* Recent Notes Section - Only shown if enabled in settings */}
{showRecentNotes && (
<RecentNotesSection
recentNotes={recentNotes.filter(note => !note.isPinned)}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
)}
{/* Main Notes Grid - Unpinned Notes Only */}
{notes.filter(note => !note.isPinned).length > 0 && (
<div data-testid="notes-grid">
<MasonryGrid
notes={notes.filter(note => !note.isPinned)}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
</div>
)}
{/* Empty state when no notes */}
{notes.filter(note => !note.isPinned).length === 0 && pinnedNotes.length === 0 && (
<div className="text-center py-8 text-gray-500">
No notes yet. Create your first note!
</div>
)}
</>
)}
{/* Memory Echo - Proactive note connections */}
<MemoryEchoNotification onOpenNote={handleOpenNote} />

View File

@@ -0,0 +1,144 @@
'use client'
import { SettingsNav, SettingsSection } from '@/components/settings'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
export default function AboutSettingsPage() {
const version = '1.0.0'
const buildDate = '2026-01-17'
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">About</h1>
<p className="text-gray-600 dark:text-gray-400">
Information about the application
</p>
</div>
<SettingsSection
title="Keep Notes"
icon={<span className="text-2xl">📝</span>}
description="A powerful note-taking application with AI-powered features"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium">Version</span>
<Badge variant="secondary">{version}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Build Date</span>
<Badge variant="outline">{buildDate}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Platform</span>
<Badge variant="outline">Web</Badge>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Features"
icon={<span className="text-2xl"></span>}
description="AI-powered capabilities"
>
<Card>
<CardContent className="pt-6 space-y-2">
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>AI-powered title suggestions</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Semantic search with embeddings</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Paragraph reformulation</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Memory Echo daily insights</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Notebook organization</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Drag & drop note management</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Label system</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Multiple AI providers (OpenAI, Ollama)</span>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Technology Stack"
icon={<span className="text-2xl"></span>}
description="Built with modern technologies"
>
<Card>
<CardContent className="pt-6 space-y-2 text-sm">
<div><strong>Frontend:</strong> Next.js 16, React 19, TypeScript</div>
<div><strong>Backend:</strong> Next.js API Routes, Server Actions</div>
<div><strong>Database:</strong> SQLite (Prisma ORM)</div>
<div><strong>Authentication:</strong> NextAuth 5</div>
<div><strong>AI:</strong> Vercel AI SDK, OpenAI, Ollama</div>
<div><strong>UI:</strong> Radix UI, Tailwind CSS, Lucide Icons</div>
<div><strong>Testing:</strong> Playwright (E2E)</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Support"
icon={<span className="text-2xl">💬</span>}
description="Get help and feedback"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div>
<p className="font-medium mb-2">Documentation</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Check the documentation for detailed guides and tutorials.
</p>
</div>
<div>
<p className="font-medium mb-2">Report Issues</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Found a bug? Report it in the issue tracker.
</p>
</div>
<div>
<p className="font-medium mb-2">Feedback</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
We value your feedback! Share your thoughts and suggestions.
</p>
</div>
</CardContent>
</Card>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,187 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingSelect, SettingInput } from '@/components/settings'
import { updateAISettings } from '@/app/actions/ai-settings'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
export default function AISettingsPage() {
const { t } = useLanguage()
const [apiKey, setApiKey] = useState('')
// Mock settings state - in real implementation, load from server
const [settings, setSettings] = useState({
titleSuggestions: true,
semanticSearch: true,
paragraphRefactor: true,
memoryEcho: true,
memoryEchoFrequency: 'daily' as 'daily' | 'weekly' | 'custom',
aiProvider: 'auto' as 'auto' | 'openai' | 'ollama',
preferredLanguage: 'auto' as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
demoMode: false
})
const handleToggle = async (feature: string, value: boolean) => {
setSettings(prev => ({ ...prev, [feature]: value }))
try {
await updateAISettings({ [feature]: value })
} catch (error) {
console.error('Error updating setting:', error)
toast.error('Failed to save setting')
setSettings(settings) // Revert on error
}
}
const handleFrequencyChange = async (value: 'daily' | 'weekly' | 'custom') => {
setSettings(prev => ({ ...prev, memoryEchoFrequency: value }))
try {
await updateAISettings({ memoryEchoFrequency: value })
} catch (error) {
console.error('Error updating frequency:', error)
toast.error('Failed to save setting')
}
}
const handleProviderChange = async (value: 'auto' | 'openai' | 'ollama') => {
setSettings(prev => ({ ...prev, aiProvider: value }))
try {
await updateAISettings({ aiProvider: value })
} catch (error) {
console.error('Error updating provider:', error)
toast.error('Failed to save setting')
}
}
const handleApiKeyChange = async (value: string) => {
setApiKey(value)
// TODO: Implement API key persistence
console.log('API Key:', value)
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">AI Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure AI-powered features and preferences
</p>
</div>
{/* AI Provider */}
<SettingsSection
title="AI Provider"
icon={<span className="text-2xl">🤖</span>}
description="Choose your preferred AI service provider"
>
<SettingSelect
label="Provider"
description="Select which AI service to use"
value={settings.aiProvider}
options={[
{
value: 'auto',
label: 'Auto-detect',
description: 'Ollama when available, OpenAI fallback'
},
{
value: 'ollama',
label: 'Ollama (Local)',
description: '100% private, runs locally on your machine'
},
{
value: 'openai',
label: 'OpenAI',
description: 'Most accurate, requires API key'
},
]}
onChange={handleProviderChange}
/>
{settings.aiProvider === 'openai' && (
<SettingInput
label="API Key"
description="Your OpenAI API key (stored securely)"
value={apiKey}
type="password"
placeholder="sk-..."
onChange={handleApiKeyChange}
/>
)}
</SettingsSection>
{/* Feature Toggles */}
<SettingsSection
title="AI Features"
icon={<span className="text-2xl"></span>}
description="Enable or disable AI-powered features"
>
<SettingToggle
label="Title Suggestions"
description="Suggest titles for untitled notes after 50+ words"
checked={settings.titleSuggestions}
onChange={(checked) => handleToggle('titleSuggestions', checked)}
/>
<SettingToggle
label="Semantic Search"
description="Search by meaning, not just keywords"
checked={settings.semanticSearch}
onChange={(checked) => handleToggle('semanticSearch', checked)}
/>
<SettingToggle
label="Paragraph Reformulation"
description="AI-powered text improvement options"
checked={settings.paragraphRefactor}
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
/>
<SettingToggle
label="Memory Echo"
description="Daily proactive note connections and insights"
checked={settings.memoryEcho}
onChange={(checked) => handleToggle('memoryEcho', checked)}
/>
{settings.memoryEcho && (
<SettingSelect
label="Memory Echo Frequency"
description="How often to analyze note connections"
value={settings.memoryEchoFrequency}
options={[
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'custom', label: 'Custom' },
]}
onChange={handleFrequencyChange}
/>
)}
</SettingsSection>
{/* Demo Mode */}
<SettingsSection
title="Demo Mode"
icon={<span className="text-2xl">🎭</span>}
description="Test AI features without using real AI calls"
>
<SettingToggle
label="Enable Demo Mode"
description="Use mock AI responses for testing and demonstrations"
checked={settings.demoMode}
onChange={(checked) => handleToggle('demoMode', checked)}
/>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,79 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingSelect } from '@/components/settings'
import { updateAISettings } from '@/app/actions/ai-settings'
export default function AppearanceSettingsPage() {
const [theme, setTheme] = useState('auto')
const [fontSize, setFontSize] = useState('medium')
const handleThemeChange = async (value: string) => {
setTheme(value)
// TODO: Implement theme persistence
console.log('Theme:', value)
}
const handleFontSizeChange = async (value: string) => {
setFontSize(value)
// TODO: Implement font size persistence
await updateAISettings({ fontSize: value as any })
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Appearance</h1>
<p className="text-gray-600 dark:text-gray-400">
Customize the look and feel of the application
</p>
</div>
<SettingsSection
title="Theme"
icon={<span className="text-2xl">🎨</span>}
description="Choose your preferred color scheme"
>
<SettingSelect
label="Color Scheme"
description="Select the app's visual theme"
value={theme}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'auto', label: 'Auto (system)' },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title="Typography"
icon={<span className="text-2xl">📝</span>}
description="Adjust text size for better readability"
>
<SettingSelect
label="Font Size"
description="Adjust the size of text throughout the app"
value={fontSize}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,200 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingInput } from '@/components/settings'
import { Button } from '@/components/ui/button'
import { Download, Upload, Trash2, Loader2, Check } from 'lucide-react'
import { toast } from 'sonner'
export default function DataSettingsPage() {
const [isExporting, setIsExporting] = useState(false)
const [isImporting, setIsImporting] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [exportUrl, setExportUrl] = useState('')
const handleExport = async () => {
setIsExporting(true)
try {
const response = await fetch('/api/notes/export')
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `keep-notes-export-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
toast.success('Notes exported successfully')
}
} catch (error) {
console.error('Export error:', error)
toast.error('Failed to export notes')
} finally {
setIsExporting(false)
}
}
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
setIsImporting(true)
try {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/notes/import', {
method: 'POST',
body: formData
})
if (response.ok) {
const result = await response.json()
toast.success(`Imported ${result.count} notes`)
// Refresh the page to show imported notes
window.location.reload()
} else {
throw new Error('Import failed')
}
} catch (error) {
console.error('Import error:', error)
toast.error('Failed to import notes')
} finally {
setIsImporting(false)
// Reset input
event.target.value = ''
}
}
const handleDeleteAll = async () => {
if (!confirm('Are you sure you want to delete all notes? This action cannot be undone.')) {
return
}
setIsDeleting(true)
try {
const response = await fetch('/api/notes/delete-all', { method: 'POST' })
if (response.ok) {
toast.success('All notes deleted')
window.location.reload()
}
} catch (error) {
console.error('Delete error:', error)
toast.error('Failed to delete notes')
} finally {
setIsDeleting(false)
}
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Data Management</h1>
<p className="text-gray-600 dark:text-gray-400">
Export, import, or manage your data
</p>
</div>
<SettingsSection
title="Export Data"
icon={<span className="text-2xl">💾</span>}
description="Download your notes as a JSON file"
>
<div className="flex items-center justify-between py-4">
<div>
<p className="font-medium">Export All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Download all your notes in JSON format
</p>
</div>
<Button
onClick={handleExport}
disabled={isExporting}
>
{isExporting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
{isExporting ? 'Exporting...' : 'Export'}
</Button>
</div>
</SettingsSection>
<SettingsSection
title="Import Data"
icon={<span className="text-2xl">📥</span>}
description="Import notes from a JSON file"
>
<div className="flex items-center justify-between py-4">
<div>
<p className="font-medium">Import Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Upload a JSON file to import notes
</p>
</div>
<div>
<input
type="file"
accept=".json"
onChange={handleImport}
disabled={isImporting}
className="hidden"
id="import-file"
/>
<Button
onClick={() => document.getElementById('import-file')?.click()}
disabled={isImporting}
>
{isImporting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Upload className="h-4 w-4 mr-2" />
)}
{isImporting ? 'Importing...' : 'Import'}
</Button>
</div>
</div>
</SettingsSection>
<SettingsSection
title="Danger Zone"
icon={<span className="text-2xl"></span>}
description="Permanently delete your data"
>
<div className="flex items-center justify-between py-4 border-t border-red-200 dark:border-red-900">
<div>
<p className="font-medium text-red-600 dark:text-red-400">Delete All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</p>
</div>
<Button
variant="destructive"
onClick={handleDeleteAll}
disabled={isDeleting}
>
{isDeleting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
{isDeleting ? 'Deleting...' : 'Delete All'}
</Button>
</div>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,107 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingSelect, SettingsSearch } from '@/components/settings'
import { useLanguage } from '@/lib/i18n'
import { updateAISettings } from '@/app/actions/ai-settings'
export default function GeneralSettingsPage() {
const { t } = useLanguage()
const [language, setLanguage] = useState('auto')
const handleLanguageChange = async (value: string) => {
setLanguage(value)
await updateAISettings({ preferredLanguage: value as any })
}
const handleNotificationsChange = async (enabled: boolean) => {
// TODO: Implement notifications setting
console.log('Notifications:', enabled)
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">General Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure basic application preferences
</p>
</div>
<SettingsSearch onSearch={(query) => console.log('Search:', query)} />
<SettingsSection
title="Language & Region"
icon={<span className="text-2xl">🌍</span>}
description="Choose your preferred language and regional settings"
>
<SettingSelect
label="Language"
description="Select the interface language"
value={language}
options={[
{ value: 'auto', label: 'Auto-detect' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fa', label: 'فارسی' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português' },
{ value: 'ru', label: 'Русский' },
{ value: 'zh', label: '中文' },
{ value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' },
{ value: 'ar', label: 'العربية' },
{ value: 'hi', label: 'हिन्दी' },
{ value: 'nl', label: 'Nederlands' },
{ value: 'pl', label: 'Polski' },
]}
onChange={handleLanguageChange}
/>
</SettingsSection>
<SettingsSection
title="Notifications"
icon={<span className="text-2xl">🔔</span>}
description="Manage how and when you receive notifications"
>
<SettingToggle
label="Email Notifications"
description="Receive email updates about your notes"
checked={false}
onChange={handleNotificationsChange}
/>
<SettingToggle
label="Desktop Notifications"
description="Show notifications in your browser"
checked={false}
onChange={handleNotificationsChange}
/>
</SettingsSection>
<SettingsSection
title="Privacy"
icon={<span className="text-2xl">🔒</span>}
description="Control your privacy settings"
>
<SettingToggle
label="Anonymous Analytics"
description="Help improve the app with anonymous usage data"
checked={false}
onChange={handleNotificationsChange}
/>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -1,184 +1,223 @@
'use client';
'use client'
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Loader2, CheckCircle, XCircle, RefreshCw, Trash2, Database, BrainCircuit } from 'lucide-react';
import { cleanupAllOrphans, syncAllEmbeddings } from '@/app/actions/notes';
import { toast } from 'sonner';
import React, { useState } from 'react'
import { SettingsNav, SettingsSection } from '@/components/settings'
import { Button } from '@/components/ui/button'
import { Loader2, CheckCircle, XCircle, RefreshCw, Database, BrainCircuit } from 'lucide-react'
import { cleanupAllOrphans, syncAllEmbeddings } from '@/app/actions/notes'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import Link from 'next/link'
export default function SettingsPage() {
const [loading, setLoading] = useState(false);
const [cleanupLoading, setCleanupLoading] = useState(false);
const [syncLoading, setSyncLoading] = useState(false);
const handleSync = async () => {
setSyncLoading(true);
try {
const result = await syncAllEmbeddings();
if (result.success) {
toast.success(`Indexing complete: ${result.count} notes processed`);
}
} catch (error) {
console.error(error);
toast.error("Error during indexing");
} finally {
setSyncLoading(false);
}
};
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [result, setResult] = useState<any>(null);
const [config, setConfig] = useState<any>(null);
const { t } = useLanguage()
const [loading, setLoading] = useState(false)
const [cleanupLoading, setCleanupLoading] = useState(false)
const [syncLoading, setSyncLoading] = useState(false)
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle')
const [result, setResult] = useState<any>(null)
const [config, setConfig] = useState<any>(null)
const checkConnection = async () => {
setLoading(true);
setStatus('idle');
setResult(null);
setLoading(true)
setStatus('idle')
setResult(null)
try {
const res = await fetch('/api/ai/test');
const data = await res.json();
const res = await fetch('/api/ai/test')
const data = await res.json()
setConfig({
provider: data.provider,
status: res.ok ? 'connected' : 'disconnected'
});
})
if (res.ok) {
setStatus('success');
setResult(data);
setStatus('success')
setResult(data)
} else {
setStatus('error');
setResult(data);
setStatus('error')
setResult(data)
}
} catch (error: any) {
console.error(error);
setStatus('error');
setResult({ message: error.message, stack: error.stack });
console.error(error)
setStatus('error')
setResult({ message: error.message, stack: error.stack })
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleCleanup = async () => {
setCleanupLoading(true);
const handleSync = async () => {
setSyncLoading(true)
try {
const result = await cleanupAllOrphans();
const result = await syncAllEmbeddings()
if (result.success) {
toast.success(result.message || `Cleanup complete: ${result.created} created, ${result.deleted} removed`);
toast.success(`Indexing complete: ${result.count} notes processed`)
}
} catch (error) {
console.error(error);
toast.error("Error during cleanup");
console.error(error)
toast.error("Error during indexing")
} finally {
setCleanupLoading(false);
setSyncLoading(false)
}
};
}
useEffect(() => {
checkConnection();
}, []);
const handleCleanup = async () => {
setCleanupLoading(true)
try {
const result = await cleanupAllOrphans()
if (result.success) {
toast.success(result.message || `Cleanup complete: ${result.created} created, ${result.deleted} removed`)
}
} catch (error) {
console.error(error)
toast.error("Error during cleanup")
} finally {
setCleanupLoading(false)
}
}
return (
<div className="container mx-auto py-10 px-4 max-w-4xl space-y-8">
<h1 className="text-3xl font-bold mb-8">Settings</h1>
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle className="flex items-center gap-2">
AI Diagnostics
{status === 'success' && <CheckCircle className="text-green-500 w-5 h-5" />}
{status === 'error' && <XCircle className="text-red-500 w-5 h-5" />}
</CardTitle>
<CardDescription>Check your AI provider connection status.</CardDescription>
</div>
<Button variant="outline" size="sm" onClick={checkConnection} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Test Connection
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Current Configuration */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">Configured Provider</p>
<p className="text-lg font-mono">{config?.provider || '...'}</p>
</div>
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">API Status</p>
<Badge variant={status === 'success' ? 'default' : 'destructive'}>
{status === 'success' ? 'Operational' : 'Error'}
</Badge>
</div>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure your application settings
</p>
</div>
{/* Test Result */}
{result && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Test Details:</h3>
<div className={`p-4 rounded-md font-mono text-xs overflow-x-auto ${status === 'error' ? 'bg-red-50 text-red-900 border border-red-200' : 'bg-slate-950 text-slate-50'}`}>
<pre>{JSON.stringify(result, null, 2)}</pre>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link href="/settings/ai">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<BrainCircuit className="h-6 w-6 text-purple-500 mb-2" />
<h3 className="font-semibold">AI Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI features and provider
</p>
</div>
</Link>
<Link href="/settings/profile">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<RefreshCw className="h-6 w-6 text-blue-500 mb-2" />
<h3 className="font-semibold">Profile Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Manage your account and preferences
</p>
</div>
</Link>
</div>
{status === 'error' && (
<div className="text-sm text-red-600 mt-2">
<p className="font-bold">Troubleshooting Tips:</p>
<ul className="list-disc list-inside mt-1">
<li>Check that Ollama is running (<code>ollama list</code>)</li>
<li>Check the URL (http://localhost:11434)</li>
<li>Verify the model (e.g., granite4:latest) is downloaded</li>
<li>Check the Next.js server terminal for more logs</li>
</ul>
{/* AI Diagnostics */}
<SettingsSection
title="AI Diagnostics"
icon={<span className="text-2xl">🔍</span>}
description="Check your AI provider connection status"
>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">Configured Provider</p>
<p className="text-lg font-mono">{config?.provider || '...'}</p>
</div>
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">API Status</p>
<div className="flex items-center gap-2">
{status === 'success' && <CheckCircle className="text-green-500 w-5 h-5" />}
{status === 'error' && <XCircle className="text-red-500 w-5 h-5" />}
<span className={`text-sm font-medium ${
status === 'success' ? 'text-green-600 dark:text-green-400' :
status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-gray-600'
}`}>
{status === 'success' ? 'Operational' :
status === 'error' ? 'Error' :
'Checking...'}
</span>
</div>
)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="w-5 h-5" />
Maintenance
</CardTitle>
<CardDescription>Tools to maintain your database health.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium">Clean Orphan Tags</h3>
<p className="text-sm text-muted-foreground">Remove tags that are no longer used by any notes.</p>
</div>
<Button variant="secondary" onClick={handleCleanup} disabled={cleanupLoading}>
{cleanupLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Trash2 className="w-4 h-4 mr-2" />}
Clean
</div>
{result && (
<div className="space-y-2 mt-4">
<h3 className="text-sm font-medium">Test Details:</h3>
<div className={`p-4 rounded-md font-mono text-xs overflow-x-auto ${
status === 'error'
? 'bg-red-50 text-red-900 border border-red-200 dark:bg-red-950 dark:text-red-100 dark:border-red-900'
: 'bg-slate-950 text-slate-50'
}`}>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
{status === 'error' && (
<div className="text-sm text-red-600 dark:text-red-400 mt-2">
<p className="font-bold">Troubleshooting Tips:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Check that Ollama is running (<code className="bg-red-100 dark:bg-red-900 px-1 rounded">ollama list</code>)</li>
<li>Check URL (http://localhost:11434)</li>
<li>Verify model (e.g., granite4:latest) is downloaded</li>
<li>Check Next.js server terminal for more logs</li>
</ul>
</div>
)}
</div>
)}
<div className="mt-4 flex justify-end">
<Button variant="outline" size="sm" onClick={checkConnection} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Test Connection
</Button>
</div>
</SettingsSection>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Semantic Indexing
<Badge variant="outline" className="text-[10px]">AI</Badge>
</h3>
<p className="text-sm text-muted-foreground">Generate vectors for all notes to enable intent-based search.</p>
{/* Maintenance */}
<SettingsSection
title="Maintenance"
icon={<span className="text-2xl">🔧</span>}
description="Tools to maintain your database health"
>
<div className="space-y-4 py-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Clean Orphan Tags
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Remove tags that are no longer used by any notes
</p>
</div>
<Button variant="secondary" onClick={handleCleanup} disabled={cleanupLoading}>
{cleanupLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Database className="w-4 h-4 mr-2" />}
Clean
</Button>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Semantic Indexing
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Generate vectors for all notes to enable intent-based search
</p>
</div>
<Button variant="secondary" onClick={handleSync} disabled={syncLoading}>
{syncLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <BrainCircuit className="w-4 h-4 mr-2" />}
Index All
</Button>
</div>
<Button variant="secondary" onClick={handleSync} disabled={syncLoading}>
{syncLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <BrainCircuit className="w-4 h-4 mr-2" />}
Index All
</Button>
</div>
</div>
</CardContent>
</Card>
</SettingsSection>
</main>
</div>
</div>
);
)
}

View File

@@ -0,0 +1,155 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingInput, SettingSelect } from '@/components/settings'
import { updateAISettings } from '@/app/actions/ai-settings'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
export default function ProfileSettingsPage() {
const { t } = useLanguage()
// Mock user data - in real implementation, load from server
const [user, setUser] = useState({
name: 'John Doe',
email: 'john@example.com'
})
const [language, setLanguage] = useState('auto')
const [showRecentNotes, setShowRecentNotes] = useState(false)
const handleNameChange = async (value: string) => {
setUser(prev => ({ ...prev, name: value }))
// TODO: Implement profile update
console.log('Name:', value)
}
const handleEmailChange = async (value: string) => {
setUser(prev => ({ ...prev, email: value }))
// TODO: Implement email update
console.log('Email:', value)
}
const handleLanguageChange = async (value: string) => {
setLanguage(value)
try {
await updateAISettings({ preferredLanguage: value as any })
} catch (error) {
console.error('Error updating language:', error)
toast.error('Failed to save language')
}
}
const handleRecentNotesChange = async (enabled: boolean) => {
setShowRecentNotes(enabled)
try {
await updateAISettings({ showRecentNotes: enabled })
} catch (error) {
console.error('Error updating recent notes setting:', error)
toast.error('Failed to save setting')
}
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Profile</h1>
<p className="text-gray-600 dark:text-gray-400">
Manage your account and personal information
</p>
</div>
{/* Profile Information */}
<SettingsSection
title="Profile Information"
icon={<span className="text-2xl">👤</span>}
description="Update your personal details"
>
<SettingInput
label="Name"
description="Your display name"
value={user.name}
onChange={handleNameChange}
placeholder="Enter your name"
/>
<SettingInput
label="Email"
description="Your email address"
value={user.email}
type="email"
onChange={handleEmailChange}
placeholder="Enter your email"
/>
</SettingsSection>
{/* Preferences */}
<SettingsSection
title="Preferences"
icon={<span className="text-2xl"></span>}
description="Customize your experience"
>
<SettingSelect
label="Language"
description="Choose your preferred language"
value={language}
options={[
{ value: 'auto', label: 'Auto-detect' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fa', label: 'فارسی' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português' },
{ value: 'ru', label: 'Русский' },
{ value: 'zh', label: '中文' },
{ value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' },
{ value: 'ar', label: 'العربية' },
{ value: 'hi', label: 'हिन्दी' },
{ value: 'nl', label: 'Nederlands' },
{ value: 'pl', label: 'Polski' },
]}
onChange={handleLanguageChange}
/>
<SettingToggle
label="Show Recent Notes"
description="Display recently viewed notes in sidebar"
checked={showRecentNotes}
onChange={handleRecentNotesChange}
/>
</SettingsSection>
{/* AI Settings Link */}
<div className="p-6 border rounded-lg bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950 dark:to-pink-950">
<div className="flex items-center gap-4">
<div className="text-4xl"></div>
<div className="flex-1">
<h3 className="font-semibold text-lg mb-1">AI Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI-powered features, provider selection, and preferences
</p>
</div>
<button
onClick={() => window.location.href = '/settings/ai'}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Configure
</button>
</div>
</div>
</main>
</div>
</div>
)
}

View File

@@ -23,11 +23,26 @@ export default async function ProfilePage() {
redirect('/login')
}
// Get user AI settings for language preference
const userAISettings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id },
select: { preferredLanguage: true }
})
// Get user AI settings for language preference and recent notes setting
let userAISettings = { preferredLanguage: 'auto', showRecentNotes: false }
try {
const result = await prisma.$queryRaw<Array<{ preferredLanguage: string | null; showRecentNotes: number | null }>>`
SELECT preferredLanguage, showRecentNotes FROM UserAISettings WHERE userId = ${session.user.id}
`
if (result && result[0]) {
// Handle NULL values - if showRecentNotes is NULL, default to false
const showRecentNotesValue = result[0].showRecentNotes !== null && result[0].showRecentNotes !== undefined
? result[0].showRecentNotes === 1
: false
userAISettings = {
preferredLanguage: result[0].preferredLanguage || 'auto',
showRecentNotes: showRecentNotesValue
}
}
} catch (error) {
// Record doesn't exist, use defaults
}
return (
<div className="container max-w-2xl mx-auto py-10 px-4">

View File

@@ -1,10 +1,12 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
@@ -12,7 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { updateProfile, changePassword, updateLanguage, updateFontSize } from '@/app/actions/profile'
import { updateProfile, changePassword, updateLanguage, updateFontSize, updateShowRecentNotes } from '@/app/actions/profile'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
@@ -36,10 +38,13 @@ const LANGUAGES = [
]
export function ProfileForm({ user, userAISettings }: { user: any; userAISettings?: any }) {
const router = useRouter()
const [selectedLanguage, setSelectedLanguage] = useState(userAISettings?.preferredLanguage || 'auto')
const [isUpdatingLanguage, setIsUpdatingLanguage] = useState(false)
const [fontSize, setFontSize] = useState(userAISettings?.fontSize || 'medium')
const [isUpdatingFontSize, setIsUpdatingFontSize] = useState(false)
const [showRecentNotes, setShowRecentNotes] = useState(userAISettings?.showRecentNotes ?? false)
const [isUpdatingRecentNotes, setIsUpdatingRecentNotes] = useState(false)
const { t } = useLanguage()
const FONT_SIZES = [
@@ -117,6 +122,28 @@ export function ProfileForm({ user, userAISettings }: { user: any; userAISetting
}
}
const handleShowRecentNotesChange = async (enabled: boolean) => {
setIsUpdatingRecentNotes(true)
const previousValue = showRecentNotes
try {
const result = await updateShowRecentNotes(enabled)
if (result?.error) {
toast.error(result.error)
} else {
setShowRecentNotes(enabled)
toast.success(t('profile.recentNotesUpdateSuccess') || 'Paramètre mis à jour')
// Force full page reload to ensure settings are reloaded
window.location.href = '/settings/profile'
}
} catch (error: any) {
setShowRecentNotes(previousValue)
toast.error(error?.message || 'Erreur')
} finally {
setIsUpdatingRecentNotes(false)
}
}
return (
<div className="space-y-6">
<Card>
@@ -213,6 +240,23 @@ export function ProfileForm({ user, userAISettings }: { user: any; userAISetting
{t('profile.fontSizeDescription')}
</p>
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="space-y-0.5">
<Label htmlFor="showRecentNotes" className="text-base font-medium">
{t('profile.showRecentNotes') || 'Afficher la section Récent'}
</Label>
<p className="text-sm text-muted-foreground">
{t('profile.showRecentNotesDescription') || 'Afficher les notes récentes (7 derniers jours) sur la page principale'}
</p>
</div>
<Switch
id="showRecentNotes"
checked={showRecentNotes}
onCheckedChange={handleShowRecentNotesChange}
disabled={isUpdatingRecentNotes}
/>
</div>
</CardContent>
</Card>

View File

@@ -0,0 +1,382 @@
'use server'
/**
* AI Server Actions Stub File
*
* This file provides a centralized location for all AI-related server action interfaces
* and serves as documentation for the AI server action architecture.
*
* IMPLEMENTATION STATUS:
* - Title Suggestions: ✅ Implemented (see app/actions/title-suggestions.ts)
* - Semantic Search: ✅ Implemented (see app/actions/semantic-search.ts)
* - Paragraph Reformulation: ✅ Implemented (see app/actions/paragraph-refactor.ts)
* - Memory Echo: ⏳ STUB - To be implemented in Epic 5 (Story 5-1)
* - Language Detection: ✅ Implemented (see app/actions/detect-language.ts)
* - AI Settings: ✅ Implemented (see app/actions/ai-settings.ts)
*
* NOTE: This file defines TypeScript interfaces and placeholder functions.
* Actual implementations are in separate action files (see references above).
*/
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
// ============================================================================
// TYPESCRIPT INTERFACES
// ============================================================================
/**
* Title Suggestions Interfaces
* @see app/actions/title-suggestions.ts for implementation
*/
export interface GenerateTitlesRequest {
noteId: string
}
export interface GenerateTitlesResponse {
suggestions: Array<{
title: string
confidence: number
reasoning?: string
}>
noteId: string
}
/**
* Semantic Search Interfaces
* @see app/actions/semantic-search.ts for implementation
*/
export interface SearchResult {
noteId: string
title: string | null
content: string
similarity: number
matchType: 'exact' | 'related'
}
export interface SemanticSearchRequest {
query: string
options?: {
limit?: number
threshold?: number
notebookId?: string
}
}
export interface SemanticSearchResponse {
results: SearchResult[]
query: string
totalResults: number
}
/**
* Paragraph Reformulation Interfaces
* @see app/actions/paragraph-refactor.ts for implementation
*/
export type RefactorMode = 'clarify' | 'shorten' | 'improve'
export interface RefactorParagraphRequest {
noteId: string
selectedText: string
option: RefactorMode
}
export interface RefactorParagraphResponse {
originalText: string
refactoredText: string
}
/**
* Memory Echo Interfaces
* STUB - To be implemented in Epic 5 (Story 5-1)
*
* This feature will analyze all user notes with embeddings to find
* connections with cosine similarity > 0.75 and provide proactive insights.
*/
export interface GenerateMemoryEchoRequest {
// No params - uses current user session
}
export interface MemoryEchoInsight {
note1Id: string
note2Id: string
similarityScore: number
}
export interface GenerateMemoryEchoResponse {
success: boolean
insight: MemoryEchoInsight | null
}
/**
* Language Detection Interfaces
* @see app/actions/detect-language.ts for implementation
*/
export interface DetectLanguageRequest {
content: string
}
export interface DetectLanguageResponse {
language: string
confidence: number
method: 'tinyld' | 'ai'
}
/**
* AI Settings Interfaces
* @see app/actions/ai-settings.ts for implementation
*/
export interface AISettingsConfig {
titleSuggestions?: boolean
semanticSearch?: boolean
paragraphRefactor?: boolean
memoryEcho?: boolean
memoryEchoFrequency?: 'daily' | 'weekly' | 'custom'
aiProvider?: 'auto' | 'openai' | 'ollama'
preferredLanguage?: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
demoMode?: boolean
}
export interface UpdateAISettingsRequest {
settings: Partial<AISettingsConfig>
}
export interface UpdateAISettingsResponse {
success: boolean
}
// ============================================================================
// PLACEHOLDER FUNCTIONS
// ============================================================================
/**
* Generate Title Suggestions
*
* ALREADY IMPLEMENTED: See app/actions/title-suggestions.ts
*
* This function generates 3 AI-powered title suggestions for a note when it
* reaches 50+ words without a title.
*
* @see generateTitleSuggestions in app/actions/title-suggestions.ts
*/
export async function generateTitles(
request: GenerateTitlesRequest
): Promise<GenerateTitlesResponse> {
// TODO: Import and use implementation from title-suggestions.ts
// import { generateTitleSuggestions } from './title-suggestions'
// return generateTitleSuggestions(request.noteId)
throw new Error('Not implemented in stub: Use app/actions/title-suggestions.ts')
}
/**
* Semantic Search
*
* ALREADY IMPLEMENTED: See app/actions/semantic-search.ts
*
* This function performs hybrid semantic + keyword search across user notes.
*
* @see semanticSearch in app/actions/semantic-search.ts
*/
export async function semanticSearch(
request: SemanticSearchRequest
): Promise<SemanticSearchResponse> {
// TODO: Import and use implementation from semantic-search.ts
// import { semanticSearch } from './semantic-search'
// return semanticSearch(request.query, request.options)
throw new Error('Not implemented in stub: Use app/actions/semantic-search.ts')
}
/**
* Refactor Paragraph
*
* ALREADY IMPLEMENTED: See app/actions/paragraph-refactor.ts
*
* This function refactors a paragraph using AI with specific mode (clarify/shorten/improve).
*
* @see refactorParagraph in app/actions/paragraph-refactor.ts
*/
export async function refactorParagraph(
request: RefactorParagraphRequest
): Promise<RefactorParagraphResponse> {
// TODO: Import and use implementation from paragraph-refactor.ts
// import { refactorParagraph } from './paragraph-refactor'
// return refactorParagraph(request.selectedText, request.option)
throw new Error('Not implemented in stub: Use app/actions/paragraph-refactor.ts')
}
/**
* Generate Memory Echo Insights
*
* STUB: To be implemented in Epic 5 (Story 5-1)
*
* This will analyze all user notes with embeddings to find
* connections with cosine similarity > 0.75.
*
* Implementation Plan:
* - Fetch all user notes with embeddings
* - Calculate pairwise cosine similarities
* - Find top connection with similarity > 0.75
* - Store in MemoryEchoInsight table
* - Return insight or null if none found
*
* @see Epic 5 Story 5-1 in planning/epics.md
*/
export async function generateMemoryEcho(): Promise<GenerateMemoryEchoResponse> {
const session = await auth()
if (!session?.user?.id) {
throw new Error('Unauthorized')
}
// TODO: Implement Memory Echo background processing
// - Fetch all user notes with embeddings from prisma.note
// - Calculate pairwise cosine similarities using embedding vectors
// - Filter for similarity > 0.75
// - Select top insight
// - Store in prisma.memoryEchoInsight table (if it exists)
// - Return { success: true, insight: {...} }
throw new Error('Not implemented: See Epic 5 Story 5-1')
}
/**
* Detect Language
*
* ALREADY IMPLEMENTED: See app/actions/detect-language.ts
*
* This function detects the language of user content.
*
* @see getInitialLanguage in app/actions/detect-language.ts
*/
export async function detectLanguage(
request: DetectLanguageRequest
): Promise<DetectLanguageResponse> {
// TODO: Import and use implementation from detect-language.ts
// import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
// const language = await detectUserLanguage()
// return { language, confidence: 0.95, method: 'tinyld' }
throw new Error('Not implemented in stub: Use app/actions/detect-language.ts')
}
/**
* Update AI Settings
*
* ALREADY IMPLEMENTED: See app/actions/ai-settings.ts
*
* This function updates user AI preferences.
*
* @see updateAISettings in app/actions/ai-settings.ts
*/
export async function updateAISettings(
request: UpdateAISettingsRequest
): Promise<UpdateAISettingsResponse> {
const session = await auth()
if (!session?.user?.id) {
throw new Error('Unauthorized')
}
// TODO: Import and use implementation from ai-settings.ts
// import { updateAISettings } from './ai-settings'
// return updateAISettings(request.settings)
throw new Error('Not implemented in stub: Use app/actions/ai-settings.ts')
}
/**
* Get AI Settings
*
* ALREADY IMPLEMENTED: See app/actions/ai-settings.ts
*
* This function retrieves user AI preferences.
*
* @see getAISettings in app/actions/ai-settings.ts
*/
export async function getAISettings(): Promise<AISettingsConfig> {
const session = await auth()
if (!session?.user?.id) {
throw new Error('Unauthorized')
}
// TODO: Import and use implementation from ai-settings.ts
// import { getAISettings } from './ai-settings'
// return getAISettings()
throw new Error('Not implemented in stub: Use app/actions/ai-settings.ts')
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Check if a specific AI feature is enabled for the user
*
* UTILITY: Helper function to check feature flags
*
* @param feature - The AI feature to check
* @returns Promise<boolean> - Whether the feature is enabled
*/
export async function isAIFeatureEnabled(
feature: keyof AISettingsConfig
): Promise<boolean> {
const session = await auth()
if (!session?.user?.id) {
return false
}
try {
const settings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
if (!settings) {
// Default to enabled for new users
return true
}
switch (feature) {
case 'titleSuggestions':
return settings.titleSuggestions ?? true
case 'semanticSearch':
return settings.semanticSearch ?? true
case 'paragraphRefactor':
return settings.paragraphRefactor ?? true
case 'memoryEcho':
return settings.memoryEcho ?? true
default:
return true
}
} catch (error) {
console.error('Error checking AI feature enabled:', error)
return true
}
}
/**
* Get user's preferred AI provider
*
* UTILITY: Helper function to get provider preference
*
* @returns Promise<'auto' | 'openai' | 'ollama'> - The AI provider
*/
export async function getUserAIPreference(): Promise<'auto' | 'openai' | 'ollama'> {
const session = await auth()
if (!session?.user?.id) {
return 'auto'
}
try {
const settings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
return (settings?.aiProvider || 'auto') as 'auto' | 'openai' | 'ollama'
} catch (error) {
console.error('Error getting user AI preference:', error)
return 'auto'
}
}

View File

@@ -13,6 +13,7 @@ export type UserAISettingsData = {
aiProvider?: 'auto' | 'openai' | 'ollama'
preferredLanguage?: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
demoMode?: boolean
showRecentNotes?: boolean
}
/**
@@ -61,17 +62,34 @@ export async function getAISettings() {
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false
demoMode: false,
showRecentNotes: false
}
}
try {
const settings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
// Use raw SQL query to get showRecentNotes until Prisma client is regenerated
const settingsRaw = await prisma.$queryRaw<Array<{
titleSuggestions: number
semanticSearch: number
paragraphRefactor: number
memoryEcho: number
memoryEchoFrequency: string
aiProvider: string
preferredLanguage: string
fontSize: string
demoMode: number
showRecentNotes: number
}>>`
SELECT titleSuggestions, semanticSearch, paragraphRefactor, memoryEcho,
memoryEchoFrequency, aiProvider, preferredLanguage, fontSize,
demoMode, showRecentNotes
FROM UserAISettings
WHERE userId = ${session.user.id}
`
// Return settings or defaults if not found
if (!settings) {
if (!settingsRaw || settingsRaw.length === 0) {
return {
titleSuggestions: true,
semanticSearch: true,
@@ -80,20 +98,29 @@ export async function getAISettings() {
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false
demoMode: false,
showRecentNotes: false
}
}
const settings = settingsRaw[0]
// Type-cast database values to proper union types
// Handle NULL values - SQLite can return NULL for showRecentNotes if column was added later
const showRecentNotesValue = settings.showRecentNotes !== null && settings.showRecentNotes !== undefined
? settings.showRecentNotes === 1
: false
return {
titleSuggestions: settings.titleSuggestions,
semanticSearch: settings.semanticSearch,
paragraphRefactor: settings.paragraphRefactor,
memoryEcho: settings.memoryEcho,
titleSuggestions: settings.titleSuggestions === 1,
semanticSearch: settings.semanticSearch === 1,
paragraphRefactor: settings.paragraphRefactor === 1,
memoryEcho: settings.memoryEcho === 1,
memoryEchoFrequency: (settings.memoryEchoFrequency || 'daily') as 'daily' | 'weekly' | 'custom',
aiProvider: (settings.aiProvider || 'auto') as 'auto' | 'openai' | 'ollama',
preferredLanguage: (settings.preferredLanguage || 'auto') as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
demoMode: settings.demoMode || false
demoMode: settings.demoMode === 1,
showRecentNotes: showRecentNotesValue
}
} catch (error) {
console.error('Error getting AI settings:', error)
@@ -106,7 +133,8 @@ export async function getAISettings() {
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false
demoMode: false,
showRecentNotes: false
}
}
}

View File

@@ -364,6 +364,7 @@ export async function createNote(data: {
await syncLabels(session.user.id, data.labels)
}
// Revalidate main page (handles both inbox and notebook views via query params)
revalidatePath('/')
return parseNote(note)
} catch (error) {
@@ -388,6 +389,7 @@ export async function updateNote(id: string, data: {
isMarkdown?: boolean
size?: 'small' | 'medium' | 'large'
autoGenerated?: boolean | null
notebookId?: string | null
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
@@ -395,12 +397,13 @@ export async function updateNote(id: string, data: {
try {
const oldNote = await prisma.note.findUnique({
where: { id, userId: session.user.id },
select: { labels: true }
select: { labels: true, notebookId: true }
})
const oldLabels: string[] = oldNote?.labels ? JSON.parse(oldNote.labels) : []
const oldNotebookId = oldNote?.notebookId
const updateData: any = { ...data }
if (data.content !== undefined) {
try {
const provider = getAIProvider(await getSystemConfig());
@@ -415,6 +418,7 @@ export async function updateNote(id: string, data: {
if ('labels' in data) updateData.labels = data.labels ? JSON.stringify(data.labels) : null
if ('images' in data) updateData.images = data.images ? JSON.stringify(data.images) : null
if ('links' in data) updateData.links = data.links ? JSON.stringify(data.links) : null
if ('notebookId' in data) updateData.notebookId = data.notebookId
updateData.updatedAt = new Date()
const note = await prisma.note.update({
@@ -428,9 +432,21 @@ export async function updateNote(id: string, data: {
await syncLabels(session.user.id, data.labels || [])
}
// Don't revalidatePath here - it would close the note editor dialog!
// The dialog will close via the onClose callback after save completes
// The UI will update via the normal React state management
// IMPORTANT: Call revalidatePath to ensure UI updates
// Revalidate main page, the note itself, and both old and new notebook paths
revalidatePath('/')
revalidatePath(`/note/${id}`)
// If notebook changed, revalidate both notebook paths
if (data.notebookId !== undefined && data.notebookId !== oldNotebookId) {
if (oldNotebookId) {
revalidatePath(`/notebook/${oldNotebookId}`)
}
if (data.notebookId) {
revalidatePath(`/notebook/${data.notebookId}`)
}
}
return parseNote(note)
} catch (error) {
console.error('Error updating note:', error)
@@ -713,6 +729,62 @@ export async function getAllNotes(includeArchived = false) {
}
}
// Get pinned notes only
export async function getPinnedNotes() {
const session = await auth();
if (!session?.user?.id) return [];
const userId = session.user.id;
try {
const notes = await prisma.note.findMany({
where: {
userId: userId,
isPinned: true,
isArchived: false
},
orderBy: [
{ order: 'asc' },
{ updatedAt: 'desc' }
]
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching pinned notes:', error)
return []
}
}
// Get recent notes (notes modified in the last 7 days)
export async function getRecentNotes(limit: number = 3) {
const session = await auth();
if (!session?.user?.id) return [];
const userId = session.user.id;
try {
const sevenDaysAgo = new Date()
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
sevenDaysAgo.setHours(0, 0, 0, 0) // Set to start of day
const notes = await prisma.note.findMany({
where: {
userId: userId,
updatedAt: { gte: sevenDaysAgo },
isArchived: false
},
orderBy: { updatedAt: 'desc' },
take: limit
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching recent notes:', error)
return []
}
}
export async function getNoteById(noteId: string) {
const session = await auth();
if (!session?.user?.id) return null;

View File

@@ -93,6 +93,8 @@ export async function updateTheme(theme: string) {
where: { id: session.user.id },
data: { theme },
})
revalidatePath('/')
revalidatePath('/settings/profile')
return { success: true }
} catch (error) {
return { error: 'Failed to update theme' }
@@ -118,6 +120,7 @@ export async function updateLanguage(language: string) {
// Note: The language will be applied on next page load
// The client component should handle updating localStorage and reloading
revalidatePath('/')
revalidatePath('/settings/profile')
return { success: true, language }
} catch (error) {
@@ -156,11 +159,13 @@ export async function updateFontSize(fontSize: string) {
memoryEcho: true,
memoryEchoFrequency: 'daily',
aiProvider: 'auto',
preferredLanguage: 'auto'
preferredLanguage: 'auto',
showRecentNotes: false
}
})
}
revalidatePath('/')
revalidatePath('/settings/profile')
return { success: true, fontSize }
} catch (error) {
@@ -168,3 +173,60 @@ export async function updateFontSize(fontSize: string) {
return { error: 'Failed to update font size' }
}
}
export async function updateShowRecentNotes(showRecentNotes: boolean) {
const session = await auth()
if (!session?.user?.id) return { error: 'Unauthorized' }
try {
// Use EXACT same pattern as updateFontSize which works
const existing = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
if (existing) {
// Try Prisma client first, fallback to raw SQL if field doesn't exist in client
try {
await prisma.userAISettings.update({
where: { userId: session.user.id },
data: { showRecentNotes: showRecentNotes } as any
})
} catch (prismaError: any) {
// If Prisma client doesn't know about showRecentNotes, use raw SQL
if (prismaError?.message?.includes('Unknown argument') || prismaError?.code === 'P2009') {
const value = showRecentNotes ? 1 : 0
await prisma.$executeRaw`
UPDATE UserAISettings
SET showRecentNotes = ${value}
WHERE userId = ${session.user.id}
`
} else {
throw prismaError
}
}
} else {
// Create new - same as updateFontSize
await prisma.userAISettings.create({
data: {
userId: session.user.id,
titleSuggestions: true,
semanticSearch: true,
paragraphRefactor: true,
memoryEcho: true,
memoryEchoFrequency: 'daily',
aiProvider: 'auto',
preferredLanguage: 'auto',
fontSize: 'medium',
showRecentNotes: showRecentNotes
} as any
})
}
revalidatePath('/')
revalidatePath('/settings/profile')
return { success: true, showRecentNotes }
} catch (error) {
console.error('[updateShowRecentNotes] Failed:', error)
return { error: 'Failed to update show recent notes setting' }
}
}

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function POST(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Delete all notes for the user (cascade will handle labels-note relationships)
const result = await prisma.note.deleteMany({
where: {
userId: session.user.id
}
})
// Delete all labels for the user
await prisma.label.deleteMany({
where: {
userId: session.user.id
}
})
// Delete all notebooks for the user
await prisma.notebook.deleteMany({
where: {
userId: session.user.id
}
})
// Revalidate paths
revalidatePath('/')
revalidatePath('/settings/data')
return NextResponse.json({
success: true,
deletedNotes: result.count
})
} catch (error) {
console.error('Delete all error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete notes' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Fetch all notes with related data
const notes = await prisma.note.findMany({
where: {
userId: session.user.id
},
include: {
labels: {
select: {
id: true,
name: true
}
},
notebook: {
select: {
id: true,
name: true
}
}
},
orderBy: {
createdAt: 'desc'
}
})
// Fetch labels separately
const labels = await prisma.label.findMany({
where: {
userId: session.user.id
},
include: {
notes: {
select: {
id: true
}
}
}
})
// Fetch notebooks
const notebooks = await prisma.notebook.findMany({
where: {
userId: session.user.id
},
include: {
notes: {
select: {
id: true
}
}
}
})
// Create export object
const exportData = {
version: '1.0.0',
exportDate: new Date().toISOString(),
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name
},
data: {
labels: labels.map(label => ({
id: label.id,
name: label.name,
color: label.color,
noteCount: label.notes.length
})),
notebooks: notebooks.map(notebook => ({
id: notebook.id,
name: notebook.name,
description: notebook.description,
noteCount: notebook.notes.length
})),
notes: notes.map(note => ({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
isPinned: note.isPinned,
notebookId: note.notebookId,
labels: note.labels.map(label => ({
id: label.id,
name: label.name
}))
}))
}
}
// Return as JSON file
const jsonString = JSON.stringify(exportData, null, 2)
return new NextResponse(jsonString, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="keep-notes-export-${new Date().toISOString().split('T')[0]}.json"`
}
})
} catch (error) {
console.error('Export error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to export notes' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,158 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function POST(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Parse form data
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ success: false, error: 'No file provided' },
{ status: 400 }
)
}
// Parse JSON file
const text = await file.text()
let importData: any
try {
importData = JSON.parse(text)
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Invalid JSON file' },
{ status: 400 }
)
}
// Validate import data structure
if (!importData.data || !importData.data.notes) {
return NextResponse.json(
{ success: false, error: 'Invalid import format' },
{ status: 400 }
)
}
let importedNotes = 0
let importedLabels = 0
let importedNotebooks = 0
// Import labels first
if (importData.data.labels && Array.isArray(importData.data.labels)) {
for (const label of importData.data.labels) {
// Check if label already exists
const existing = await prisma.label.findFirst({
where: {
userId: session.user.id,
name: label.name
}
})
if (!existing) {
await prisma.label.create({
data: {
userId: session.user.id,
name: label.name,
color: label.color
}
})
importedLabels++
}
}
}
// Import notebooks
const notebookIdMap = new Map<string, string>()
if (importData.data.notebooks && Array.isArray(importData.data.notebooks)) {
for (const notebook of importData.data.notebooks) {
// Check if notebook already exists
const existing = await prisma.notebook.findFirst({
where: {
userId: session.user.id,
name: notebook.name
}
})
let newNotebookId
if (!existing) {
const created = await prisma.notebook.create({
data: {
userId: session.user.id,
name: notebook.name,
description: notebook.description || null,
position: 0
}
})
newNotebookId = created.id
notebookIdMap.set(notebook.id, newNotebookId)
importedNotebooks++
} else {
notebookIdMap.set(notebook.id, existing.id)
}
}
}
// Import notes
if (importData.data.notes && Array.isArray(importData.data.notes)) {
for (const note of importData.data.notes) {
// Map notebook ID
const mappedNotebookId = notebookIdMap.get(note.notebookId) || null
// Get label IDs
const labels = await prisma.label.findMany({
where: {
userId: session.user.id,
name: {
in: note.labels.map((l: any) => l.name)
}
}
})
// Create note
await prisma.note.create({
data: {
userId: session.user.id,
title: note.title || 'Untitled',
content: note.content,
isPinned: note.isPinned || false,
notebookId: mappedNotebookId,
labels: {
connect: labels.map(label => ({ id: label.id }))
}
}
})
importedNotes++
}
}
// Revalidate paths
revalidatePath('/')
revalidatePath('/settings/data')
return NextResponse.json({
success: true,
count: importedNotes,
labels: importedLabels,
notebooks: importedNotebooks
})
} catch (error) {
console.error('Import error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to import notes' },
{ status: 500 }
)
}
}