+ {/* Profile info card */}
+
+
+
+
-
-
-
+
+
{t('profile.title')}
+
{t('profile.description')}
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+ {/* Password card */}
+
+
+
+
+
+
+
{t('profile.changePassword')}
+
{t('profile.changePasswordDescription')}
+
+
+
+
+
)
}
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')}
-
+
-
-
-
-
-
- {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 */}