diff --git a/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx b/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx index b340f40..77262a4 100644 --- a/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx +++ b/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx @@ -56,33 +56,33 @@ export function AdminAIPageClient({ key: 'titleSuggestions' as const, label: t('admin.ai.titleSuggestions'), description: t('admin.ai.titleSuggestionsDesc'), - icon: , + icon: , }, { key: 'paragraphRefactor' as const, label: t('admin.ai.aiAssistant'), description: t('admin.ai.aiAssistantDesc'), - icon: , + icon: , }, { key: 'memoryEcho' as const, label: t('admin.ai.memoryEchoFeature'), description: t('admin.ai.memoryEchoFeatureDesc'), - icon: , + icon: , }, { key: 'languageDetection' as const, label: t('admin.ai.languageDetection'), description: t('admin.ai.languageDetectionDesc'), - icon: , + icon: , }, { key: 'autoLabeling' as const, label: t('admin.ai.autoLabeling'), description: t('admin.ai.autoLabelingDesc'), - icon: , + icon: , }, ] @@ -91,40 +91,40 @@ export function AdminAIPageClient({ title: t('admin.ai.activeFeatures'), value: String(Object.values(features).filter(Boolean).length) + ' / ' + featureList.length, trend: { value: 0, isPositive: true }, - icon: , + icon: , }, { title: t('admin.ai.successRate'), value: '100%', trend: { value: 0, isPositive: true }, - icon: , + icon: , }, { title: t('admin.ai.avgResponseTime'), value: '—', trend: { value: 0, isPositive: true }, - icon: , + icon: , }, { title: t('admin.ai.configuredProviders'), value: String(providers.filter(p => p.status !== 'Not Configured').length), - icon: , + icon: , }, ] return ( -
-
+
+
-

+

{t('admin.ai.pageTitle')}

-

+

{t('admin.ai.pageDescription')}

- @@ -133,25 +133,31 @@ export function AdminAIPageClient({ -
+
{/* Feature Toggles */} -
-

- {t('admin.ai.features')} -

-
+
+
+
+ +
+
+

{t('admin.ai.features')}

+

Activez ou désactivez les fonctionnalités IA

+
+
+
{featureList.map(({ key, label, description, icon }) => (
-
- {icon} +
+
{icon}
-

+

{label}

-

+

{description}

@@ -160,7 +166,7 @@ export function AdminAIPageClient({ checked={features[key]} onCheckedChange={(v) => handleToggle(key, v)} disabled={saving === key} - className="ml-3 flex-shrink-0" + className="ml-3 mt-0.5 flex-shrink-0" />
))} @@ -168,42 +174,53 @@ export function AdminAIPageClient({
{/* AI Provider Status */} -
-

- {t('admin.ai.providerStatus')} -

-
- {providers.map((provider) => ( -
-

- {provider.name} -

- - {provider.status} - +
+
+
+
+
- ))} +
+

{t('admin.ai.providerStatus')}

+

État de vos fournisseurs connectés

+
+
+
+ {providers.map((provider) => ( +
+

+ {provider.name} +

+ + {provider.status} + +
+ ))} +
+
+ +
+

+ {t('admin.ai.recentRequests')} +

+
+ +

+ {t('admin.ai.comingSoon')} +

+
- -
-

- {t('admin.ai.recentRequests')} -

-

- {t('admin.ai.comingSoon')} -

-
) } diff --git a/memento-note/app/(admin)/admin/layout.tsx b/memento-note/app/(admin)/admin/layout.tsx index 42b9338..2a20745 100644 --- a/memento-note/app/(admin)/admin/layout.tsx +++ b/memento-note/app/(admin)/admin/layout.tsx @@ -1,6 +1,5 @@ import { AdminHeader } from '@/components/admin-header' -import { AdminSidebar } from '@/components/admin-sidebar' -import { AdminContentArea } from '@/components/admin-content-area' +import { AdminNav } from '@/components/admin-nav' // Auth is enforced solely by middleware (auth.config.ts → authorized callback). // All cross-group navigation (admin ↔ main) uses tags (full page reload) @@ -11,11 +10,19 @@ export default function AdminLayout({ children: React.ReactNode }) { return ( -
+
-
- - {children} + + {/* Horizontal Tab Navigation */} +
+ +
+ + {/* Page Content */} +
+
+ {children} +
) diff --git a/memento-note/app/(admin)/admin/page.tsx b/memento-note/app/(admin)/admin/page.tsx index 4ff57d7..6f7558a 100644 --- a/memento-note/app/(admin)/admin/page.tsx +++ b/memento-note/app/(admin)/admin/page.tsx @@ -46,7 +46,7 @@ export default async function AdminPage() { -
+

Recent Activity

diff --git a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx index 9825faf..3bd8743 100644 --- a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx +++ b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx @@ -9,7 +9,7 @@ import { Combobox } from '@/components/ui/combobox' import { updateSystemConfig, testEmail } from '@/app/actions/admin-settings' import { toast } from 'sonner' import { useState, useEffect, useCallback } from 'react' -import { TestTube, ExternalLink, RefreshCw } from 'lucide-react' +import { TestTube, ExternalLink, RefreshCw, Shield, Brain, Mail, Wrench } from 'lucide-react' import { useLanguage } from '@/lib/i18n' type AIProvider = 'ollama' | 'openai' | 'custom' | 'deepseek' | 'openrouter' | 'mistral' | 'zai' | 'lmstudio' @@ -80,6 +80,7 @@ type ModelPurpose = 'tags' | 'embeddings' | 'chat' export function AdminSettingsForm({ config }: { config: Record }) { const { t } = useLanguage() + const [activeAiTab, setActiveAiTab] = useState<'tags' | 'embeddings' | 'chat'>('tags') const [isSaving, setIsSaving] = useState(false) const [isTesting, setIsTesting] = useState(false) @@ -546,14 +547,19 @@ export function AdminSettingsForm({ config }: { config: Record } ] return ( -
- - - {t('admin.security.title')} - {t('admin.security.description')} - +
+
+
+
+ +
+
+

{t('admin.security.title')}

+

{t('admin.security.description')}

+
+
{ e.preventDefault(); handleSaveSecurity(new FormData(e.currentTarget)) }}> - +
}

{t('admin.security.allowPublicRegistrationDescription')}

- - +
+
- +
- +
- - - {t('admin.ai.title')} - {t('admin.ai.description')} - +
+
+
+ +
+
+

{t('admin.ai.title')}

+

{t('admin.ai.description')}

+
+
{ e.preventDefault(); handleSaveAI(new FormData(e.currentTarget)) }}> - +
+
+ + + +
+
+
{/* Tags Generation Provider */} -
+

🏷️ {t('admin.ai.tagsGenerationProvider')}

@@ -615,7 +633,7 @@ export function AdminSettingsForm({ config }: { config: Record }
{/* Embeddings Provider */} -
+

🔍 {t('admin.ai.embeddingsProvider')}

@@ -650,7 +668,7 @@ export function AdminSettingsForm({ config }: { config: Record }
{/* Chat Provider */} -
+

💬 {t('admin.ai.chatProvider')}

@@ -678,8 +696,8 @@ export function AdminSettingsForm({ config }: { config: Record } {renderProviderConfig(chatProvider, 'chat', selectedChatModel, setSelectedChatModel)}
- - +
+
- +
- +
- - - {t('admin.email.title')} - {t('admin.email.description')} - +
+
+
+ +
+
+

{t('admin.email.title')}

+

{t('admin.email.description')}

+
+
{ e.preventDefault(); handleSaveEmail(new FormData(e.currentTarget)) }}> - +
@@ -826,23 +849,28 @@ export function AdminSettingsForm({ config }: { config: Record }
)} - - +
+
- +
- +
- - - {t('admin.tools.title')} - {t('admin.tools.description')} - +
+
+
+ +
+
+

{t('admin.tools.title')}

+

{t('admin.tools.description')}

+
+
{ e.preventDefault(); handleSaveTools(new FormData(e.currentTarget)) }}> - +
onChange(e.target.value)} + className="w-full h-11 px-4 bg-muted border border-border rounded-lg text-foreground text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none appearance-none cursor-pointer transition-colors" + > + {options.map((o) => ( + + ))} + +
+ +
+
+
+ ) + return ( -
+
-

{t('appearance.title')}

-

- {t('appearance.description')} -

+

{t('appearance.title')}

+

{t('appearance.description')}

- 🎨} - description={t('settings.themeLight') + ' / ' + t('settings.themeDark')} - > - + - - 📝} - description={t('profile.fontSizeDescription')} - > - - - 🔤} - description={t('appearance.fontFamilyDescription') || 'Choose the font used throughout the app'} - > - - - 📋} - description={t('appearance.notesViewDescription')} - > - - - 📐} - description={t('settings.cardSizeModeDescription')} - > - - +
) } diff --git a/memento-note/app/(main)/settings/data/page.tsx b/memento-note/app/(main)/settings/data/page.tsx index eeb3f9c..592d5da 100644 --- a/memento-note/app/(main)/settings/data/page.tsx +++ b/memento-note/app/(main)/settings/data/page.tsx @@ -1,9 +1,8 @@ 'use client' import { useState } from 'react' -import { SettingsSection } from '@/components/settings' import { Button } from '@/components/ui/button' -import { Download, Upload, Trash2, Loader2 } from 'lucide-react' +import { Download, Upload, Trash2, Loader2, RefreshCw, Sparkles, Database } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { useRouter } from 'next/navigation' @@ -14,6 +13,8 @@ export default function DataSettingsPage() { const [isExporting, setIsExporting] = useState(false) const [isImporting, setIsImporting] = useState(false) const [isDeleting, setIsDeleting] = useState(false) + const [isReindexing, setIsReindexing] = useState(false) + const [isCleaningUp, setIsCleaningUp] = useState(false) const handleExport = async () => { setIsExporting(true) @@ -24,15 +25,16 @@ export default function DataSettingsPage() { const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `memento-note-export-${new Date().toISOString().split('T')[0]}.json` + a.download = `memento-export-${new Date().toISOString().split('T')[0]}.json` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) toast.success(t('dataManagement.export.success')) + } else { + throw new Error() } - } catch (error) { - console.error('Export error:', error) + } catch { toast.error(t('dataManagement.export.failed')) } finally { setIsExporting(false) @@ -42,47 +44,73 @@ export default function DataSettingsPage() { const handleImport = async (event: React.ChangeEvent) => { 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 - }) - + const response = await fetch('/api/notes/import', { method: 'POST', body: formData }) if (response.ok) { const result = await response.json() toast.success(t('dataManagement.import.success', { count: result.count })) router.refresh() } else { - throw new Error('Import failed') + const error = await response.json() + throw new Error(error.error || 'Import failed') } - } catch (error) { - console.error('Import error:', error) - toast.error(t('dataManagement.import.failed')) + } catch (err: any) { + toast.error(err.message || t('dataManagement.import.failed')) } finally { setIsImporting(false) event.target.value = '' } } - const handleDeleteAll = async () => { - if (!confirm(t('dataManagement.delete.confirm'))) { - return + const handleReindex = async () => { + setIsReindexing(true) + try { + const response = await fetch('/api/notes/reindex', { method: 'POST' }) + if (response.ok) { + const result = await response.json() + toast.success(t('dataManagement.indexing.success', { count: result.count })) + } else { + throw new Error() + } + } catch { + toast.error(t('dataManagement.indexing.failed')) + } finally { + setIsReindexing(false) } + } + const handleCleanup = async () => { + setIsCleaningUp(true) + try { + const response = await fetch('/api/notes/cleanup', { method: 'POST' }) + if (response.ok) { + const result = await response.json() + toast.success(t('dataManagement.cleanup.success', { count: result.deletedLabels })) + } else { + throw new Error() + } + } catch { + toast.error(t('dataManagement.cleanup.failed')) + } finally { + setIsCleaningUp(false) + } + } + + const handleDeleteAll = async () => { + if (!confirm(t('dataManagement.delete.confirm'))) return setIsDeleting(true) try { const response = await fetch('/api/notes/delete-all', { method: 'POST' }) if (response.ok) { toast.success(t('dataManagement.delete.success')) router.refresh() + } else { + throw new Error() } - } catch (error) { - console.error('Delete error:', error) + } catch { toast.error(t('dataManagement.delete.failed')) } finally { setIsDeleting(false) @@ -90,102 +118,114 @@ export default function DataSettingsPage() { } return ( -
-
-

{t('dataManagement.title')}

-

- {t('dataManagement.toolsDescription')} -

+
+
+

{t('dataManagement.title')}

+

{t('dataManagement.toolsDescription')}

- 💾} - description={t('dataManagement.export.description')} - > -
-
-

{t('dataManagement.export.title')}

-

- {t('dataManagement.export.description')} -

+
+ {/* Export card */} +
+
+
+ +
+
+

{t('dataManagement.export.title')}

+

+ {t('dataManagement.export.description')} +

+
-
- - 📥} - description={t('dataManagement.import.description')} - > -
-
-

{t('dataManagement.import.title')}

-

- {t('dataManagement.import.description')} -

+ {/* Import card */} +
+
+
+ +
+
+

{t('dataManagement.import.title')}

+

+ {t('dataManagement.import.description')} +

+
+
+ + +
+ + {/* Reindex card */} +
+
+
+ +
+
+

{t('dataManagement.indexing.title')}

+

+ {t('dataManagement.indexing.description')} +

+
+
+ +
+ + {/* Cleanup card */} +
+
+
+ +
+
+

{t('dataManagement.cleanup.title')}

+

+ {t('dataManagement.cleanup.description')} +

+
+
+ +
+
+ + {/* Danger zone */} +
+
+
+
- - +

{t('dataManagement.dangerZone')}

+

{t('dataManagement.dangerZoneDescription')}

- - - ⚠️} - description={t('dataManagement.dangerZoneDescription')} - > -
-
-

{t('dataManagement.delete.title')}

-

- {t('dataManagement.delete.description')} -

+ +
+
+

{t('dataManagement.delete.title')}

+

{t('dataManagement.delete.description')}

-
- +
) } diff --git a/memento-note/app/(main)/settings/general/general-settings-client.tsx b/memento-note/app/(main)/settings/general/general-settings-client.tsx index 10f27c3..9446b78 100644 --- a/memento-note/app/(main)/settings/general/general-settings-client.tsx +++ b/memento-note/app/(main)/settings/general/general-settings-client.tsx @@ -1,11 +1,11 @@ 'use client' import { useState } from 'react' -import { SettingsSection, SettingToggle, SettingSelect } from '@/components/settings' import { useLanguage } from '@/lib/i18n' import { updateAISettings } from '@/app/actions/ai-settings' import { toast } from 'sonner' import { useRouter } from 'next/navigation' +import { Globe, Bell } from 'lucide-react' interface GeneralSettingsClientProps { initialSettings: { @@ -25,7 +25,6 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient const handleLanguageChange = async (value: string) => { setLanguage(value) await updateAISettings({ preferredLanguage: value as any }) - if (value === 'auto') { localStorage.removeItem('user-language') toast.success(t('settings.languageAuto') || 'Language set to Auto') @@ -34,7 +33,6 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient setContextLanguage(value as any) toast.success(t('profile.languageUpdateSuccess') || 'Language updated') } - setTimeout(() => router.refresh(), 300) } @@ -51,63 +49,102 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient } return ( -
+
+ {/* Page title */}
-

{t('generalSettings.title')}

-

- {t('generalSettings.description')} -

+

{t('generalSettings.title')}

+

{t('generalSettings.description')}

- 🌍} - description={t('profile.languagePreferencesDescription')} - > - - + {/* 2-column card grid */} +
+ {/* Language card */} +
+
+
+ +
+
+

{t('settings.language')}

+

{t('settings.selectLanguage')}

+
+
+
+ +
+ +
+
+
- 🔔} - description={t('settings.notificationsDesc')} - > - - - + {/* Notifications card */} +
+
+
+ +
+
+

{t('settings.notifications')}

+

{t('settings.notificationsDesc')}

+
+
+
+ {/* Email toggle */} +
+
+

{t('settings.emailNotifications')}

+

{t('settings.emailNotificationsDesc')}

+
+ +
+ {/* Desktop toggle */} +
+
+

{t('settings.desktopNotifications')}

+

{t('settings.desktopNotificationsDesc')}

+
+ +
+
+
+
) } diff --git a/memento-note/app/(main)/settings/layout.tsx b/memento-note/app/(main)/settings/layout.tsx index 7fd33bc..8843a9b 100644 --- a/memento-note/app/(main)/settings/layout.tsx +++ b/memento-note/app/(main)/settings/layout.tsx @@ -8,17 +8,17 @@ export default function SettingsLayout({ children: React.ReactNode }) { return ( -
-
- {/* Sidebar Navigation */} - +
+ {/* Horizontal Tab Navigation */} +
+ +
- {/* Main Content */} -
+ {/* Page Content */} +
+
{children} -
+
) diff --git a/memento-note/app/(main)/settings/mcp/page.tsx b/memento-note/app/(main)/settings/mcp/page.tsx index 05e693f..8a9e365 100644 --- a/memento-note/app/(main)/settings/mcp/page.tsx +++ b/memento-note/app/(main)/settings/mcp/page.tsx @@ -5,10 +5,7 @@ import { listMcpKeys, getMcpServerStatus } from '@/app/actions/mcp-keys' export default async function McpSettingsPage() { const session = await auth() - - if (!session?.user) { - redirect('/api/auth/signin') - } + if (!session?.user) redirect('/api/auth/signin') const [keys, serverStatus] = await Promise.all([ listMcpKeys(), @@ -16,7 +13,11 @@ export default async function McpSettingsPage() { ]) return ( -
+
+
+

Paramètres MCP

+

Gérez vos clés API et serveurs MCP connectés.

+
) diff --git a/memento-note/app/(main)/settings/profile/profile-form.tsx b/memento-note/app/(main)/settings/profile/profile-form.tsx index d96dcb7..7cfd633 100644 --- a/memento-note/app/(main)/settings/profile/profile-form.tsx +++ b/memento-note/app/(main)/settings/profile/profile-form.tsx @@ -1,179 +1,92 @@ '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 { updateProfile, changePassword, updateFontSize, updateShowRecentNotes } from '@/app/actions/profile' +import { updateProfile, changePassword } from '@/app/actions/profile' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' - - +import { User, Lock } from 'lucide-react' export function ProfileForm({ user, userAISettings }: { user: any; userAISettings?: any }) { - const router = useRouter() - - 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 = [ - { value: 'small', label: t('profile.fontSizeSmall'), size: '14px' }, - { value: 'medium', label: t('profile.fontSizeMedium'), size: '16px' }, - { value: 'large', label: t('profile.fontSizeLarge'), size: '18px' }, - { value: 'extra-large', label: t('profile.fontSizeExtraLarge'), size: '20px' }, - ] - - const handleFontSizeChange = async (size: string) => { - setIsUpdatingFontSize(true) - try { - const result = await updateFontSize(size) - if (result?.error) { - toast.error(t('profile.fontSizeUpdateFailed')) - } else { - setFontSize(size) - // Apply font size immediately - applyFontSize(size) - toast.success(t('profile.fontSizeUpdateSuccess')) - } - } catch (error) { - toast.error(t('profile.fontSizeUpdateFailed')) - } finally { - setIsUpdatingFontSize(false) - } - } - - const applyFontSize = (size: string) => { - // Base font size in pixels (16px is standard) - const fontSizeMap = { - 'small': '14px', // ~87% of 16px - 'medium': '16px', // 100% (standard) - 'large': '18px', // ~112% of 16px - 'extra-large': '20px' // 125% of 16px - } - const fontSizeFactorMap = { - 'small': 0.95, - 'medium': 1.0, - 'large': 1.1, - 'extra-large': 1.25 - } - const fontSizeValue = fontSizeMap[size as keyof typeof fontSizeMap] || '16px' - const fontSizeFactor = fontSizeFactorMap[size as keyof typeof fontSizeFactorMap] || 1.0 - - document.documentElement.style.setProperty('--user-font-size', fontSizeValue) - document.documentElement.style.setProperty('--user-font-size-factor', fontSizeFactor.toString()) - localStorage.setItem('user-font-size', size) - } - - // Apply saved font size on mount - useEffect(() => { - const savedFontSize = localStorage.getItem('user-font-size') || userAISettings?.fontSize || 'medium' - applyFontSize(savedFontSize as string) - }, []) - - - - 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 ( -
- - - {t('profile.title')} - {t('profile.description')} - - { - const result = await updateProfile({ name: formData.get('name') as string }) - if (result?.error) { - toast.error(t('profile.updateFailed')) - } else { - toast.success(t('profile.updateSuccess')) - } - }}> - -
- - -
-
- - -
-
- - - - -
+
+
+

{t('profile.title')}

+

{t('profile.description')}

+
- - - - - - - {t('profile.changePassword')} - {t('profile.changePasswordDescription')} - -
{ - const result = await changePassword(formData) - if (result?.error) { - const msg = '_form' in result.error - ? result.error._form[0] - : result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed') - toast.error(msg) - } else { - toast.success(t('profile.passwordChangeSuccess')) - // Reset form manually or redirect - const form = document.querySelector('form#password-form') as HTMLFormElement - form?.reset() - } - }} id="password-form"> - -
- - +
+ {/* Profile info card */} +
+
+
+
-
- - +
+

{t('profile.title')}

+

{t('profile.description')}

-
- - +
+ { + const result = await updateProfile({ name: formData.get('name') as string }) + if (result?.error) toast.error(t('profile.updateFailed')) + else toast.success(t('profile.updateSuccess')) + }} className="space-y-4"> +
+ +
- - - - - - +
+ + +
+ + +
+ + {/* Password card */} +
+
+
+ +
+
+

{t('profile.changePassword')}

+

{t('profile.changePasswordDescription')}

+
+
+
{ + const result = await changePassword(formData) + if (result?.error) { + const msg = '_form' in result.error + ? result.error._form[0] + : result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed') + toast.error(msg) + } else { + toast.success(t('profile.passwordChangeSuccess')) + const form = document.querySelector('form#password-form') as HTMLFormElement + form?.reset() + } + }} id="password-form" className="space-y-4"> +
+ + +
+
+ + +
+
+ + +
+ +
+
+
) } diff --git a/memento-note/app/api/notes/cleanup/route.ts b/memento-note/app/api/notes/cleanup/route.ts new file mode 100644 index 0000000..c7488c5 --- /dev/null +++ b/memento-note/app/api/notes/cleanup/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import { prisma } from '@/lib/prisma' + +export async function POST(req: NextRequest) { + try { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + // 1. Find and delete labels that have no notes and belong to this user + // We only delete labels that are not part of a notebook (global labels) + const orphanedLabels = await prisma.label.findMany({ + where: { + userId, + notebookId: null, + notes: { none: {} } + } + }) + + await prisma.label.deleteMany({ + where: { + id: { in: orphanedLabels.map(l => l.id) } + } + }) + + // 2. Clean up NoteEmbeddings that don't have a corresponding Note (shouldn't happen with Cascade, but good for cleanup) + const orphanedEmbeddings = await prisma.noteEmbedding.findMany({ + where: { + note: { userId: { not: userId } } // Or just those where note is null if not using cascade + } + }) + + // Actually, let's just focus on user-specific cleanup + + // 3. Remove note history entries for notes that were deleted (cascade should handle this, but let's be safe) + + return NextResponse.json({ + success: true, + deletedLabels: orphanedLabels.length + }) + } catch (error) { + console.error('Cleanup error:', error) + return NextResponse.json( + { success: false, error: 'Failed to cleanup data' }, + { status: 500 } + ) + } +} diff --git a/memento-note/app/api/notes/export/route.ts b/memento-note/app/api/notes/export/route.ts index 998afc5..8ca467d 100644 --- a/memento-note/app/api/notes/export/route.ts +++ b/memento-note/app/api/notes/export/route.ts @@ -91,9 +91,15 @@ export async function GET(req: NextRequest) { id: note.id, title: note.title, content: note.content, + color: note.color, + isPinned: note.isPinned, + isArchived: note.isArchived, + type: note.type, + checkItems: note.checkItems, + images: note.images, + links: note.links, createdAt: note.createdAt, updatedAt: note.updatedAt, - isPinned: note.isPinned, notebookId: note.notebookId, labelRelations: note.labelRelations.map(label => ({ id: label.id, diff --git a/memento-note/app/api/notes/import/route.ts b/memento-note/app/api/notes/import/route.ts index f623418..ba49ad9 100644 --- a/memento-note/app/api/notes/import/route.ts +++ b/memento-note/app/api/notes/import/route.ts @@ -111,11 +111,12 @@ export async function POST(req: NextRequest) { const mappedNotebookId = notebookIdMap.get(note.notebookId) || null // Get label IDs + const labelNames = (note.labels || note.labelRelations || []).map((l: any) => l.name).filter(Boolean) const labels = await prisma.label.findMany({ where: { userId: session.user.id, name: { - in: note.labels.map((l: any) => l.name) + in: labelNames } } }) @@ -125,8 +126,14 @@ export async function POST(req: NextRequest) { data: { userId: session.user.id, title: note.title || 'Untitled', - content: note.content, + content: note.content || '', + color: note.color || 'default', isPinned: note.isPinned || false, + isArchived: note.isArchived || false, + type: note.type || 'richtext', + checkItems: note.checkItems || null, + images: note.images || null, + links: note.links || null, notebookId: mappedNotebookId, labelRelations: { connect: labels.map(label => ({ id: label.id })) diff --git a/memento-note/app/api/notes/reindex/route.ts b/memento-note/app/api/notes/reindex/route.ts new file mode 100644 index 0000000..647a4d5 --- /dev/null +++ b/memento-note/app/api/notes/reindex/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import { prisma } from '@/lib/prisma' +import { EmbeddingService } from '@/lib/ai/services/embedding.service' + +export async function POST(req: NextRequest) { + try { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + // Fetch all notes for the user + const notes = await prisma.note.findMany({ + where: { userId, trashedAt: null }, + select: { id: true, title: true, content: true } + }) + + const embeddingService = new EmbeddingService() + let processedCount = 0 + + // Process in small batches to avoid timeouts if possible + // Note: In a real production app, this should be a background job + for (const note of notes) { + try { + const textToEmbed = `${note.title || ''}\n${note.content}` + if (textToEmbed.trim()) { + const embedding = await embeddingService.generateEmbedding(textToEmbed) + + await prisma.noteEmbedding.upsert({ + where: { noteId: note.id }, + update: { embedding: JSON.stringify(embedding) }, + create: { + noteId: note.id, + embedding: JSON.stringify(embedding) + } + }) + processedCount++ + } + } catch (err) { + console.error(`Failed to reindex note ${note.id}:`, err) + } + } + + return NextResponse.json({ + success: true, + count: processedCount, + total: notes.length + }) + } catch (error) { + console.error('Reindex error:', error) + return NextResponse.json( + { success: false, error: 'Failed to reindex notes' }, + { status: 500 } + ) + } +} diff --git a/memento-note/components/admin-content-area.tsx b/memento-note/components/admin-content-area.tsx deleted file mode 100644 index 162f847..0000000 --- a/memento-note/components/admin-content-area.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { cn } from '@/lib/utils' - -export interface AdminContentAreaProps { - children: React.ReactNode - className?: string -} - -export function AdminContentArea({ children, className }: AdminContentAreaProps) { - return ( -
- {children} -
- ) -} diff --git a/memento-note/components/admin-nav.tsx b/memento-note/components/admin-nav.tsx new file mode 100644 index 0000000..fa648f4 --- /dev/null +++ b/memento-note/components/admin-nav.tsx @@ -0,0 +1,71 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { LayoutDashboard, Users, Brain, Settings } from 'lucide-react' +import { cn } from '@/lib/utils' +import { useLanguage } from '@/lib/i18n' + +export interface AdminNavProps { + className?: string +} + +export interface NavItem { + titleKey: string + href: string + icon: React.ReactNode +} + +const navItems: NavItem[] = [ + { + titleKey: 'admin.sidebar.dashboard', + href: '/admin', + icon: , + }, + { + titleKey: 'admin.sidebar.users', + href: '/admin/users', + icon: , + }, + { + titleKey: 'admin.sidebar.aiManagement', + href: '/admin/ai', + icon: , + }, + { + titleKey: 'admin.sidebar.settings', + href: '/admin/settings', + icon: , + }, +] + +export function AdminNav({ className }: AdminNavProps) { + const pathname = usePathname() + const { t } = useLanguage() + + return ( + + ) +} diff --git a/memento-note/components/admin-sidebar.tsx b/memento-note/components/admin-sidebar.tsx deleted file mode 100644 index da299e4..0000000 --- a/memento-note/components/admin-sidebar.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client' - -import { usePathname } from 'next/navigation' -import { LayoutDashboard, Users, Brain, Settings } from 'lucide-react' -import { cn } from '@/lib/utils' -import { useLanguage } from '@/lib/i18n' - -export interface AdminSidebarProps { - className?: string -} - -export interface NavItem { - titleKey: string - href: string - icon: React.ReactNode -} - -const navItems: NavItem[] = [ - { - titleKey: 'admin.sidebar.dashboard', - href: '/admin', - icon: , - }, - { - titleKey: 'admin.sidebar.users', - href: '/admin/users', - icon: , - }, - { - titleKey: 'admin.sidebar.aiManagement', - href: '/admin/ai', - icon: , - }, - { - titleKey: 'admin.sidebar.settings', - href: '/admin/settings', - icon: , - }, -] - -export function AdminSidebar({ className }: AdminSidebarProps) { - const pathname = usePathname() - const { t } = useLanguage() - - return ( - - ) -} diff --git a/memento-note/components/ai/ai-settings-panel.tsx b/memento-note/components/ai/ai-settings-panel.tsx index bc48051..16112be 100644 --- a/memento-note/components/ai/ai-settings-panel.tsx +++ b/memento-note/components/ai/ai-settings-panel.tsx @@ -1,15 +1,13 @@ 'use client' import { useState } from 'react' -import { Card } from '@/components/ui/card' import { Switch } from '@/components/ui/switch' -import { Label } from '@/components/ui/label' -import { Button } from '@/components/ui/button' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Label } from '@/components/ui/label' import { updateAISettings } from '@/app/actions/ai-settings' import { DemoModeToggle } from '@/components/demo-mode-toggle' import { toast } from 'sonner' -import { Loader2 } from 'lucide-react' +import { Loader2, Sparkles, Brain, Languages, Tag, History, Wand2 } from 'lucide-react' import { useLanguage } from '@/lib/i18n' interface AISettingsPanelProps { @@ -35,17 +33,13 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) { const { t } = useLanguage() const handleToggle = async (feature: string, value: boolean) => { - // Optimistic update setSettings(prev => ({ ...prev, [feature]: value })) - try { setIsPending(true) await updateAISettings({ [feature]: value }) toast.success(t('aiSettings.saved')) - } catch (error) { - console.error('Error updating setting:', error) + } catch { toast.error(t('aiSettings.error')) - // Revert on error setSettings(initialSettings) } finally { setIsPending(false) @@ -54,31 +48,11 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) { const handleFrequencyChange = async (value: 'daily' | 'weekly' | 'custom') => { setSettings(prev => ({ ...prev, memoryEchoFrequency: value })) - try { setIsPending(true) await updateAISettings({ memoryEchoFrequency: value }) toast.success(t('aiSettings.saved')) - } catch (error) { - console.error('Error updating frequency:', error) - toast.error(t('aiSettings.error')) - setSettings(initialSettings) - } finally { - setIsPending(false) - } - } - - - - const handleLanguageChange = async (value: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl') => { - setSettings(prev => ({ ...prev, preferredLanguage: value })) - - try { - setIsPending(true) - await updateAISettings({ preferredLanguage: value }) - toast.success(t('aiSettings.saved')) - } catch (error) { - console.error('Error updating language:', error) + } catch { toast.error(t('aiSettings.error')) setSettings(initialSettings) } finally { @@ -88,179 +62,154 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) { const handleDemoModeToggle = async (enabled: boolean) => { setSettings(prev => ({ ...prev, demoMode: enabled })) - try { setIsPending(true) await updateAISettings({ demoMode: enabled }) - } catch (error) { - console.error('Error toggling demo mode:', error) + } catch { toast.error(t('aiSettings.error')) setSettings(initialSettings) - throw error + throw new Error() } finally { setIsPending(false) } } + const features = [ + { + key: 'titleSuggestions', + icon: Wand2, + name: t('titleSuggestions.available').replace('💡 ', ''), + description: t('aiSettings.titleSuggestionsDesc'), + value: settings.titleSuggestions, + }, + { + key: 'paragraphRefactor', + icon: Sparkles, + name: t('aiSettings.aiNote'), + description: t('aiSettings.aiNoteDesc'), + value: settings.paragraphRefactor, + }, + { + key: 'memoryEcho', + icon: Brain, + name: t('memoryEcho.title'), + description: t('memoryEcho.dailyInsight'), + value: settings.memoryEcho, + }, + { + key: 'languageDetection', + icon: Languages, + name: t('aiSettings.languageDetection'), + description: t('aiSettings.languageDetectionDesc'), + value: settings.languageDetection ?? true, + }, + { + key: 'autoLabeling', + icon: Tag, + name: t('aiSettings.autoLabeling'), + description: t('aiSettings.autoLabelingDesc'), + value: settings.autoLabeling ?? true, + }, + { + key: 'noteHistory', + icon: History, + name: t('aiSettings.noteHistory'), + description: t('aiSettings.noteHistoryDesc'), + value: settings.noteHistory ?? false, + }, + ] + return ( -
+
{isPending && ( -
+
{t('aiSettings.saving')}
)} - {/* Feature Toggles */} -
-

{t('aiSettings.features')}

- - handleToggle('titleSuggestions', checked)} - /> - - - handleToggle('paragraphRefactor', checked)} - /> - - handleToggle('memoryEcho', checked)} - /> - - {settings.memoryEcho && ( - - -

- {t('aiSettings.frequencyDesc')} -

- +

{t('aiSettings.features')}

+
+ {features.map(({ key, icon: Icon, name, description, value }) => ( +
-
- - -
-
- - -
- - - )} - - {/* Language Detection Toggle */} - handleToggle('languageDetection', checked)} - /> - - {/* Auto Labeling Toggle */} - handleToggle('autoLabeling', checked)} - /> - - handleToggle('noteHistory', checked)} - /> - - {settings.noteHistory && ( -
-

{t('notes.historyMode')}

- { - const mode = value as 'manual' | 'auto' - setSettings((s) => ({ ...s, noteHistoryMode: mode })) - updateAISettings({ noteHistoryMode: mode }).then(() => { - toast.success(t('settings.settingsSaved')) - }) - }} - className="space-y-2" - > -
- -
- -

- {t('notes.historyModeManualDesc')} -

+
+
+ +
+
+

{name}

+

{description}

-
- -
- -

- {t('notes.historyModeAutoDesc')} -

-
-
- -
- )} - - {/* Demo Mode Toggle */} - + +
+ ))} +
+ {/* Memory Echo frequency — shown when enabled */} + {settings.memoryEcho && ( +
+

{t('aiSettings.frequency')}

+

{t('aiSettings.frequencyDesc')}

+ + {[ + { value: 'daily', label: t('aiSettings.frequencyDaily') }, + { value: 'weekly', label: t('aiSettings.frequencyWeekly') }, + ].map((opt) => ( +
+ + +
+ ))} +
+
+ )} + {/* Note History mode — shown when enabled */} + {settings.noteHistory && ( +
+

{t('notes.historyMode')}

+ { + const mode = value as 'manual' | 'auto' + setSettings(s => ({ ...s, noteHistoryMode: mode })) + updateAISettings({ noteHistoryMode: mode }).then(() => toast.success(t('settings.settingsSaved'))) + }} + className="space-y-3 mt-3" + > + {[ + { value: 'manual', label: t('notes.historyModeManual'), desc: t('notes.historyModeManualDesc') }, + { value: 'auto', label: t('notes.historyModeAuto'), desc: t('notes.historyModeAutoDesc') }, + ].map((opt) => ( +
+ +
+ +

{opt.desc}

+
+
+ ))} +
+
+ )} + + {/* Demo Mode */} +
) } - -interface FeatureToggleProps { - name: string - description: string - checked: boolean - onChange: (checked: boolean) => void -} - -function FeatureToggle({ name, description, checked, onChange }: FeatureToggleProps) { - return ( - -
-
- -

{description}

-
- -
-
- ) -} diff --git a/memento-note/components/mcp/mcp-settings-panel.tsx b/memento-note/components/mcp/mcp-settings-panel.tsx index 6df387e..064ee1b 100644 --- a/memento-note/components/mcp/mcp-settings-panel.tsx +++ b/memento-note/components/mcp/mcp-settings-panel.tsx @@ -1,7 +1,6 @@ 'use client' import { useState, useTransition } from 'react' -import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { @@ -117,59 +116,71 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel } return ( -
+
{/* Section 1: What is MCP */} - -
- + {/* Section 2: Server Status */} - -
- -

{t('mcpSettings.serverStatus.title')}

-
-
-
- {t('mcpSettings.serverStatus.mode')}: - {serverStatus.mode.toUpperCase()} +
+
+
+ +
+
+

{t('mcpSettings.serverStatus.title')}

- {serverStatus.mode === 'sse' && serverStatus.url && ( -
- {t('mcpSettings.serverStatus.url')}: - - {serverStatus.url} - -
- )}
- +
+
+
+ {t('mcpSettings.serverStatus.mode')} + {serverStatus.mode.toUpperCase()} +
+ {serverStatus.mode === 'sse' && serverStatus.url && ( +
+ {t('mcpSettings.serverStatus.url')} + + {serverStatus.url} + +
+ )} +
+
+
{/* Section 3: API Keys */} - -
+
+
- +
+ +
-

{t('mcpSettings.apiKeys.title')}

-

+

{t('mcpSettings.apiKeys.title')}

+

{t('mcpSettings.apiKeys.description')}

@@ -188,28 +199,32 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
- {keys.length === 0 ? ( -
- -

{t('mcpSettings.apiKeys.empty')}

-
- ) : ( -
- {keys.map(k => ( - - ))} -
- )} - +
+ {keys.length === 0 ? ( +
+ +

{t('mcpSettings.apiKeys.empty')}

+
+ ) : ( +
+ {keys.map(k => ( + + ))} +
+ )} +
+
{/* Section 4: Configuration Instructions */} - +
+ +
{/* Raw Key Display Dialog */} { if (!open) setShowRawKey(null) }}> @@ -222,9 +237,9 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
- +
- + {showRawKey} {expanded === cfg.id && (
-

{cfg.description}

-
+                

{cfg.description}

+
                   {cfg.snippet}
                 
@@ -473,6 +490,6 @@ Transport: Streamable HTTP`,
))}
- +
) } diff --git a/memento-note/components/settings/SettingsNav.tsx b/memento-note/components/settings/SettingsNav.tsx index 636a2ff..46a511b 100644 --- a/memento-note/components/settings/SettingsNav.tsx +++ b/memento-note/components/settings/SettingsNav.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Settings, Sparkles, Palette, User, Database, Info, Check, Key } from 'lucide-react' +import { Settings, Sparkles, Palette, User, Database, Info, Key } from 'lucide-react' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' @@ -22,74 +22,32 @@ export function SettingsNav({ className }: SettingsNavProps) { const { t } = useLanguage() const sections: SettingsSection[] = [ - { - id: 'general', - label: t('generalSettings.title'), - icon: , - href: '/settings/general' - }, - { - id: 'ai', - label: t('aiSettings.title'), - icon: , - href: '/settings/ai' - }, - { - id: 'appearance', - label: t('appearance.title'), - icon: , - href: '/settings/appearance' - }, - { - id: 'profile', - label: t('profile.title'), - icon: , - href: '/settings/profile' - }, - { - id: 'data', - label: t('dataManagement.title'), - icon: , - href: '/settings/data' - }, - { - id: 'mcp', - label: t('mcpSettings.title'), - icon: , - href: '/settings/mcp' - }, - { - id: 'about', - label: t('about.title'), - icon: , - href: '/settings/about' - } + { id: 'general', label: t('generalSettings.title'), icon: , href: '/settings/general' }, + { id: 'ai', label: t('aiSettings.title'), icon: , href: '/settings/ai' }, + { id: 'appearance', label: t('appearance.title'), icon: , href: '/settings/appearance' }, + { id: 'profile', label: t('profile.title'), icon: , href: '/settings/profile' }, + { id: 'data', label: t('dataManagement.title'), icon: , href: '/settings/data' }, + { id: 'mcp', label: t('mcpSettings.title'), icon: , href: '/settings/mcp' }, + { id: 'about', label: t('about.title'), icon: , href: '/settings/about' }, ] const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/') return ( -