diff --git a/memento-note/app/(main)/page.tsx b/memento-note/app/(main)/page.tsx index 49c507a..f94863a 100644 --- a/memento-note/app/(main)/page.tsx +++ b/memento-note/app/(main)/page.tsx @@ -13,9 +13,7 @@ export default async function HomePage() { ? ('masonry' as const) : settings?.notesViewMode === 'tabs' ? ('tabs' as const) - : settings?.notesViewMode === 'list' - ? ('list' as const) - : ('masonry' as const) + : ('masonry' as const) return ( () + const roots: any[] = [] + for (const nb of notebooks) { + map.set(nb.id, { ...nb, children: [] }) + } + for (const nb of notebooks) { + const node = map.get(nb.id)! + if (nb.parentId && map.has(nb.parentId)) { + map.get(nb.parentId)!.children.push(node) + } else { + roots.push(node) + } + } + return roots +} + export async function GET(request: NextRequest) { const session = await auth() if (!session?.user?.id) { @@ -17,9 +33,7 @@ export async function GET(request: NextRequest) { const notebooks = await prisma.notebook.findMany({ where: { userId: session.user.id }, include: { - labels: { - orderBy: { name: 'asc' } - }, + labels: { orderBy: { name: 'asc' } }, _count: { select: { notes: { where: { isArchived: false, trashedAt: null } } } } @@ -27,23 +41,20 @@ export async function GET(request: NextRequest) { orderBy: { order: 'asc' } }) - return NextResponse.json({ - success: true, - notebooks: notebooks.map(nb => ({ - ...nb, - notesCount: nb._count.notes - })) - }) + const flat = notebooks.map(nb => ({ + ...nb, + notesCount: nb._count.notes + })) + + const tree = buildTree(flat) + + return NextResponse.json({ success: true, notebooks: flat, tree }) } catch (error) { console.error('Error fetching notebooks:', error) - return NextResponse.json( - { success: false, error: 'Failed to fetch notebooks' }, - { status: 500 } - ) + return NextResponse.json({ success: false, error: 'Failed to fetch notebooks' }, { status: 500 }) } } -// POST /api/notebooks - Create a new notebook export async function POST(request: NextRequest) { const session = await auth() if (!session?.user?.id) { @@ -52,42 +63,49 @@ export async function POST(request: NextRequest) { try { const body = await request.json() - const { name, icon, color } = body + const { name, icon, color, parentId } = body if (!name || typeof name !== 'string') { - return NextResponse.json( - { success: false, error: 'Notebook name is required' }, - { status: 400 } - ) + return NextResponse.json({ success: false, error: 'Notebook name is required' }, { status: 400 }) } - // Get the highest order value for this user + if (parentId) { + const parent = await prisma.notebook.findFirst({ + where: { id: parentId, userId: session.user.id } + }) + if (!parent) { + return NextResponse.json({ success: false, error: 'Parent notebook not found' }, { status: 400 }) + } + } + + const whereClause: any = { userId: session.user.id } + if (parentId) whereClause.parentId = parentId + else whereClause.parentId = null + const highestOrder = await prisma.notebook.findFirst({ - where: { userId: session.user.id }, + where: whereClause, orderBy: { order: 'desc' }, select: { order: true } }) const nextOrder = (highestOrder?.order ?? -1) + 1 - // Create notebook const notebook = await prisma.notebook.create({ data: { name: name.trim(), icon: icon || DEFAULT_ICONS[Math.floor(Math.random() * DEFAULT_ICONS.length)], color: color || DEFAULT_COLORS[Math.floor(Math.random() * DEFAULT_COLORS.length)], order: nextOrder, + parentId: parentId || null, userId: session.user.id }, include: { labels: true, - _count: { - select: { notes: { where: { isArchived: false, trashedAt: null } } } - } + _count: { select: { notes: { where: { isArchived: false, trashedAt: null } } } } } }) - revalidatePath('/') + try { revalidatePath('/') } catch {} return NextResponse.json({ success: true, @@ -96,9 +114,6 @@ export async function POST(request: NextRequest) { }, { status: 201 }) } catch (error) { console.error('Error creating notebook:', error) - return NextResponse.json( - { success: false, error: 'Failed to create notebook' }, - { status: 500 } - ) + return NextResponse.json({ success: false, error: 'Failed to create notebook' }, { status: 500 }) } } diff --git a/memento-note/components/admin-page-header.tsx b/memento-note/components/admin-page-header.tsx deleted file mode 100644 index 44aa268..0000000 --- a/memento-note/components/admin-page-header.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import { useLanguage } from '@/lib/i18n' - -export function AdminPageHeader() { - const { t } = useLanguage() - - return ( -

{t('nav.userManagement')}

- ) -} - -export function SettingsButton() { - const { t } = useLanguage() - - return t('settings.title') -} diff --git a/memento-note/components/agents/agent-form.tsx b/memento-note/components/agents/agent-form.tsx deleted file mode 100644 index 11fbb76..0000000 --- a/memento-note/components/agents/agent-form.tsx +++ /dev/null @@ -1,782 +0,0 @@ -'use client' - -/** - * Agent Form Component - * Simplified form for creating and editing agents. - * Novice-friendly: hides system prompt and tools behind "Advanced mode". - */ - -import { useState, useMemo, useRef, useCallback, useEffect } from 'react' -import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon, Presentation, Pencil, Check } from 'lucide-react' -import { toast } from 'sonner' -import { useLanguage } from '@/lib/i18n' -import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' - -// --- Types --- - -type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator' - -/** Small "?" tooltip shown next to form labels */ -function FieldHelp({ tooltip }: { tooltip: string }) { - return ( - - - - - - {tooltip} - - - ) -} - -interface AgentFormProps { - agent?: { - id: string - name: string - description?: string | null - type?: string | null - role: string - sourceUrls?: string | null - sourceNotebookId?: string | null - sourceNoteIds?: string | null - targetNotebookId?: string | null - frequency: string - tools?: string | null - maxSteps?: number - notifyEmail?: boolean - includeImages?: boolean - scheduledTime?: string | null - scheduledDay?: number | null - timezone?: string | null - slideTheme?: string | null - slideStyle?: string | null - } | null - notebooks: { id: string; name: string; icon?: string | null }[] - onSave: (data: FormData) => Promise - onCancel: () => void -} - -// --- Tool presets per type --- -const TOOL_PRESETS: Record = { - scraper: ['web_scrape', 'note_create', 'memory_search'], - researcher: ['web_search', 'web_scrape', 'note_search', 'note_create', 'memory_search'], - monitor: ['note_search', 'note_read', 'note_create', 'memory_search'], - custom: ['memory_search'], - 'slide-generator': ['generate_pptx'], - 'excalidraw-generator': ['generate_excalidraw'], -} - -// --- Shared class strings --- -const labelCls = 'block text-sm font-medium text-foreground mb-1.5' -const labelCls2 = 'block text-sm font-medium text-foreground mb-2' -const inputCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground' -const selectCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground' -const toggleOffBorder = 'border-border hover:border-primary/30' -const toggleOffIcon = 'text-muted-foreground' -const toggleOffLabel = 'text-sm font-medium text-foreground' -const toggleOffHint = 'text-xs text-muted-foreground' - -// --- Component --- - -export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps) { - const { t } = useLanguage() - const [name, setName] = useState(agent?.name || '') - const [description, setDescription] = useState(agent?.description || '') - const [type, setType] = useState((agent?.type as AgentType) || 'scraper') - const [role, setRole] = useState(agent?.role || '') - const [urls, setUrls] = useState(() => { - if (agent?.sourceUrls) { - try { return JSON.parse(agent.sourceUrls) } catch { return [''] } - } - return [''] - }) - const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '') - const [sourceNoteIds, setSourceNoteIds] = useState(() => { - if (agent?.sourceNoteIds) { - try { return JSON.parse(agent.sourceNoteIds) } catch { return [] } - } - return [] - }) - const [noteOptions, setNoteOptions] = useState<{ id: string; title: string }[]>([]) - const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '') - const [frequency, setFrequency] = useState(agent?.frequency || 'manual') - const [scheduledTime, setScheduledTime] = useState(agent?.scheduledTime || '08:00') - const [scheduledDay, setScheduledDay] = useState(agent?.scheduledDay ?? 1) - const [timezone] = useState(() => { - try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' } - }) - const [selectedTools, setSelectedTools] = useState(() => { - if (agent?.tools) { - try { - const parsed = JSON.parse(agent.tools) - if (parsed.length > 0) return parsed - } catch { /* fall through to presets */ } - } - // New agent or old agent with empty tools: use preset defaults - const defaultType = (agent?.type as AgentType) || 'scraper' - return TOOL_PRESETS[defaultType] || [] - }) - const [maxSteps, setMaxSteps] = useState(agent?.maxSteps || 10) - const [notifyEmail, setNotifyEmail] = useState(agent?.notifyEmail || false) - const [includeImages, setIncludeImages] = useState(agent?.includeImages || false) - const [slideTheme, setSlideTheme] = useState(agent?.slideTheme || '') - const [slideStyle, setSlideStyle] = useState<'soft' | 'sharp' | 'rounded' | 'pill'>( - (agent?.slideStyle as 'soft' | 'sharp' | 'rounded' | 'pill') || 'soft' - ) - const [excalidrawStyle, setExcalidrawStyle] = useState<'default' | 'austere' | 'sketch-plus'>(() => { - if (agent?.slideStyle === 'austere') return 'austere' - if (agent?.slideStyle === 'sketch-plus') return 'sketch-plus' - return 'default' - }) - const [excalidrawType, setExcalidrawType] = useState<'auto' | 'architecture-cloud' | 'flowchart' | 'mindmap' | 'org-chart' | 'timeline' | 'process-map'>(() => { - const value = (agent?.slideTheme || '').trim() - if (value === 'auto' || value === 'architecture-cloud' || value === 'mindmap' || value === 'org-chart' || value === 'timeline' || value === 'process-map') return value - return 'flowchart' - }) - const [isSaving, setIsSaving] = useState(false) - - useEffect(() => { - if (!sourceNotebookId || (type !== 'slide-generator' && type !== 'excalidraw-generator' && type !== 'monitor')) { - setNoteOptions([]) - return - } - fetch(`/api/notes?notebookId=${sourceNotebookId}&limit=50`) - .then(r => r.json()) - .then(data => { - const notes = Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : [] - setNoteOptions(notes.map((n: any) => ({ id: n.id, title: n.title || 'Sans titre' }))) - }) - .catch(() => setNoteOptions([])) - }, [sourceNotebookId, type]) - - const [showAdvanced, setShowAdvanced] = useState(() => { - // Auto-open advanced if editing an agent with custom tools or custom prompt - if (agent?.tools) { - try { - const tools = JSON.parse(agent.tools) - if (tools.length > 0) return true - } catch { /* ignore */ } - } - // Also open if agent has a custom role (instructions) - if (agent?.role && agent.role.trim().length > 0) return true - return false - }) - - // Tool definitions - const availableTools = useMemo(() => [ - { id: 'web_search', icon: Globe, labelKey: 'agents.tools.webSearch', external: true }, - { id: 'web_scrape', icon: ExternalLink, labelKey: 'agents.tools.webScrape', external: true }, - { id: 'note_search', icon: FileSearch, labelKey: 'agents.tools.noteSearch', external: false }, - { id: 'note_read', icon: FileText, labelKey: 'agents.tools.noteRead', external: false }, - { id: 'note_create', icon: FilePlus, labelKey: 'agents.tools.noteCreate', external: false }, - { id: 'url_fetch', icon: ExternalLink, labelKey: 'agents.tools.urlFetch', external: false }, - { id: 'memory_search', icon: Brain, labelKey: 'agents.tools.memorySearch', external: false }, - { id: 'generate_pptx', icon: Presentation, labelKey: 'agents.tools.generatePptx', external: false }, - { id: 'generate_slides', icon: Presentation, labelKey: 'agents.tools.generateSlides', external: false }, - { id: 'generate_excalidraw', icon: Pencil, labelKey: 'agents.tools.generateExcalidraw', external: false }, - ], []) - - // Track previous type to detect user-initiated type changes - const prevTypeRef = useRef(type) - - // When user explicitly changes type (not on mount), reset tools to presets - if (prevTypeRef.current !== type) { - prevTypeRef.current = type - // This is a user-initiated type change, not a mount - // We queue the state update to happen after render - setSelectedTools(TOOL_PRESETS[type] || []) - setRole('') - } - - const addUrl = () => setUrls([...urls, '']) - const removeUrl = (index: number) => setUrls(urls.filter((_, i) => i !== index)) - const updateUrl = (index: number, value: string) => { - const newUrls = [...urls] - newUrls[index] = value - setUrls(newUrls) - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!name.trim()) { - toast.error(t('agents.form.nameRequired')) - return - } - - setIsSaving(true) - try { - const formData = new FormData() - formData.set('name', name.trim()) - formData.set('description', description.trim()) - formData.set('type', type) - formData.set('role', role || t(`agents.defaultRoles.${type}`)) - formData.set('frequency', frequency) - formData.set('targetNotebookId', targetNotebookId) - - if (type === 'monitor' || type === 'slide-generator' || type === 'excalidraw-generator') { - formData.set('sourceNotebookId', sourceNotebookId) - } - - if (sourceNoteIds.length > 0) { - formData.set('sourceNoteIds', JSON.stringify(sourceNoteIds)) - } - - const validUrls = urls.filter(u => u.trim()) - if (validUrls.length > 0) { - formData.set('sourceUrls', JSON.stringify(validUrls)) - } - - formData.set('tools', JSON.stringify(selectedTools)) - formData.set('maxSteps', String(maxSteps)) - formData.set('notifyEmail', String(notifyEmail)) - formData.set('includeImages', String(includeImages)) - formData.set('scheduledTime', scheduledTime) - formData.set('scheduledDay', String(scheduledDay)) - formData.set('timezone', timezone) - - if (type === 'slide-generator') { - if (slideTheme) formData.set('slideTheme', slideTheme) - formData.set('slideStyle', slideStyle) - } - if (type === 'excalidraw-generator') { - formData.set('slideTheme', excalidrawType) - formData.set('slideStyle', excalidrawStyle) - } - - await onSave(formData) - } catch { - toast.error(t('agents.toasts.saveError')) - } finally { - setIsSaving(false) - } - } - - const showSourceNotebook = type === 'monitor' || type === 'slide-generator' || type === 'excalidraw-generator' - - const agentTypes: { value: AgentType; labelKey: string; descKey: string }[] = [ - { value: 'researcher', labelKey: 'agents.types.researcher', descKey: 'agents.typeDescriptions.researcher' }, - { value: 'scraper', labelKey: 'agents.types.scraper', descKey: 'agents.typeDescriptions.scraper' }, - { value: 'monitor', labelKey: 'agents.types.monitor', descKey: 'agents.typeDescriptions.monitor' }, - { value: 'slide-generator', labelKey: 'agents.types.slideGenerator', descKey: 'agents.typeDescriptions.slideGenerator' }, - { value: 'excalidraw-generator', labelKey: 'agents.types.excalidrawGenerator', descKey: 'agents.typeDescriptions.excalidrawGenerator' }, - { value: 'custom', labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom' }, - ] - - return ( -
-
e.stopPropagation()} - > - {/* Header β€” editable agent name */} -
- setName(e.target.value)} - className="text-lg font-semibold text-card-foreground bg-transparent border-none outline-none focus:ring-0 p-0 flex-1 placeholder:text-muted-foreground/40" - placeholder={t('agents.form.namePlaceholder')} - required - /> - -
- -
- {/* Agent Type */} -
- -
- {agentTypes.map(at => ( - - ))} -
-
- - {/* Research Topic (researcher only) β€” replaces Description for this type */} - {type === 'researcher' && ( -
- - setDescription(e.target.value)} - className={inputCls} - placeholder={t('agents.form.researchTopicPlaceholder')} - /> -
- )} - - {/* Description (for non-researcher types) */} - {type !== 'researcher' && ( -
- - setDescription(e.target.value)} - className={inputCls} - placeholder={t('agents.form.descriptionPlaceholder')} - /> -
- )} - - {/* URLs (scraper and custom only β€” researcher uses search, not URLs) */} - {(type === 'scraper' || type === 'custom') && ( -
- -
- {urls.map((url, i) => ( -
- updateUrl(i, e.target.value)} - className={inputCls} - placeholder="https://example.com" - /> - {urls.length > 1 && ( - - )} -
- ))} - -
-
- )} - - {/* Source Notebook (monitor, slide, excalidraw) */} - {showSourceNotebook && ( -
- - -
- )} - - {/* Note multi-select (slide-generator, excalidraw-generator only) */} - {(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && ( -
- -
- {noteOptions.map(note => { - const isSelected = sourceNoteIds.includes(note.id) - return ( - - ) - })} -
- {sourceNoteIds.length > 0 && ( -

{t('agents.form.notesSelected', { count: sourceNoteIds.length })}

- )} -
- )} - - {/* Theme selector β€” slide-generator only */} - {type === 'slide-generator' && ( - <> -
- - -
-
- -
- {(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => ( - - ))} -
-
- - )} - - {/* Visual style selector β€” excalidraw-generator only */} - {type === 'excalidraw-generator' && ( - <> -
- -
- {([ - { id: 'auto', labelKey: 'agents.form.excalidrawDiagramTypeAuto' }, - { id: 'flowchart', labelKey: 'agents.form.excalidrawDiagramTypeFlowchart' }, - { id: 'mindmap', labelKey: 'agents.form.excalidrawDiagramTypeMindmap' }, - { id: 'org-chart', labelKey: 'agents.form.excalidrawDiagramTypeOrgChart' }, - { id: 'timeline', labelKey: 'agents.form.excalidrawDiagramTypeTimeline' }, - { id: 'process-map', labelKey: 'agents.form.excalidrawDiagramTypeProcessMap' }, - { id: 'architecture-cloud', labelKey: 'agents.form.excalidrawDiagramTypeArchitectureCloud' }, - ] as const).map((opt) => ( - - ))} -
-
-
- -
- {([ - { id: 'default', labelKey: 'agents.form.excalidrawDiagramStyleDefault' }, - { id: 'sketch-plus', labelKey: 'agents.form.excalidrawDiagramStyleSketchPlus' }, - { id: 'austere', labelKey: 'agents.form.excalidrawDiagramStyleAustere' }, - ] as const).map((opt) => ( - - ))} -
-
- - )} - - {/* Target Notebook β€” hidden for file generators (they never create notes) */} - {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( -
- - -
- )} - - {/* Frequency */} -
- - -
- - {/* Schedule config: time + day pickers (hidden for manual/hourly) */} - {frequency !== 'manual' && frequency !== 'hourly' && ( -
- {/* Day selector (weekly/monthly only) */} - {frequency === 'weekly' && ( -
- - -
- )} - {frequency === 'monthly' && ( -
- - -
- )} - {/* Time picker */} -
- - setScheduledTime(e.target.value)} - className={inputCls} - /> -
-
- )} - - {/* Email Notification β€” hidden for file generators */} - {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( -
setNotifyEmail(!notifyEmail)} - className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ - notifyEmail - ? 'border-primary bg-primary/5' - : toggleOffBorder - }`} - > - -
-
{t('agents.form.notifyEmail')}
-
{t('agents.form.notifyEmailHint')}
-
-
-
-
-
- )} - - {/* Include Images β€” hidden for file generators */} - {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( -
setIncludeImages(!includeImages)} - className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ - includeImages - ? 'border-primary bg-primary/5' - : toggleOffBorder - }`} - > - -
-
{t('agents.form.includeImages')}
-
{t('agents.form.includeImagesHint')}
-
-
-
-
-
- )} - - {/* Advanced mode toggle */} - - - {/* Advanced: System Prompt */} - {showAdvanced && ( - <> -
- -