diff --git a/memento-note/app/api/ai/auto-labels/route.ts b/memento-note/app/api/ai/auto-labels/route.ts index 9a5e475..59aa32c 100644 --- a/memento-note/app/api/ai/auto-labels/route.ts +++ b/memento-note/app/api/ai/auto-labels/route.ts @@ -69,13 +69,8 @@ export async function POST(request: NextRequest) { data: suggestions, }) } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to get label suggestions', - }, - { status: 500 } - ) + console.error('[/api/ai/auto-labels] POST failed:', error) + return NextResponse.json({ success: true, data: null }) } } @@ -118,12 +113,7 @@ export async function PUT(request: NextRequest) { }, }) } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to create labels', - }, - { status: 500 } - ) + console.error('[/api/ai/auto-labels] PUT failed:', error) + return NextResponse.json({ success: false, error: 'Failed to create labels' }) } } diff --git a/memento-note/app/api/ai/tags/route.ts b/memento-note/app/api/ai/tags/route.ts index 217feb8..bf12adf 100644 --- a/memento-note/app/api/ai/tags/route.ts +++ b/memento-note/app/api/ai/tags/route.ts @@ -30,31 +30,38 @@ export async function POST(req: NextRequest) { // If notebookId is provided, use contextual suggestions (IA2) if (notebookId) { - const suggestions = await contextualAutoTagService.suggestLabels( - content, - notebookId, - session.user.id, - language - ); + try { + const suggestions = await contextualAutoTagService.suggestLabels( + content, + notebookId, + session.user.id, + language + ); - // Convert label → tag to match TagSuggestion interface - const convertedTags = suggestions.map(s => ({ - tag: s.label, // Convert label to tag - confidence: s.confidence, - // Keep additional properties for client-side use - ...(s.reasoning && { reasoning: s.reasoning }), - ...(s.isNewLabel !== undefined && { isNewLabel: s.isNewLabel }) - })); + const convertedTags = suggestions.map(s => ({ + tag: s.label, + confidence: s.confidence, + ...(s.reasoning && { reasoning: s.reasoning }), + ...(s.isNewLabel !== undefined && { isNewLabel: s.isNewLabel }) + })); - return NextResponse.json({ tags: convertedTags }); + return NextResponse.json({ tags: convertedTags }); + } catch (err) { + console.error('[/api/ai/tags] contextualAutoTag failed:', err) + return NextResponse.json({ tags: [] }); + } } // Otherwise, use legacy auto-tagging (generates new tags) - const config = await getSystemConfig(); - const provider = getTagsProvider(config); - const tags = await provider.generateTags(content, language); - - return NextResponse.json({ tags }); + try { + const config = await getSystemConfig(); + const provider = getTagsProvider(config); + const tags = await provider.generateTags(content, language); + return NextResponse.json({ tags }); + } catch (err) { + console.error('[/api/ai/tags] legacy tagging failed:', err) + return NextResponse.json({ tags: [] }); + } } catch (error: any) { if (error instanceof z.ZodError) { diff --git a/memento-note/app/api/labels/[id]/route.ts b/memento-note/app/api/labels/[id]/route.ts index 7bea138..0adc12c 100644 --- a/memento-note/app/api/labels/[id]/route.ts +++ b/memento-note/app/api/labels/[id]/route.ts @@ -114,7 +114,10 @@ export async function PUT( for (const note of allNotes) { if (note.labels) { try { - const noteLabels: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] + const parsed = typeof note.labels === 'string' ? JSON.parse(note.labels) : note.labels + const noteLabels: string[] = Array.isArray(parsed) + ? parsed.filter((n): n is string => typeof n === 'string') + : [] const updatedLabels = noteLabels.map(l => l.toLowerCase() === currentLabel.name.toLowerCase() ? newName : l ) @@ -123,8 +126,8 @@ export async function PUT( await prisma.note.update({ where: { id: note.id }, data: { - labels: updatedLabels as any - } + labels: updatedLabels.length > 0 ? JSON.stringify(updatedLabels) : null, + }, }) } } catch (e) { @@ -197,36 +200,50 @@ export async function DELETE( return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - // For backward compatibility, remove from old label field in notes + // Remove label from all notes that have it (JSON + labelRelations) const targetUserIdDel = label.userId || label.notebook?.userId || session.user.id; if (targetUserIdDel) { const allNotes = await prisma.note.findMany({ where: { userId: targetUserIdDel, - labels: { not: null } + OR: [ + { labels: { not: null } }, + { labelRelations: { some: { id: label.id } } }, + ], }, select: { id: true, labels: true } }) for (const note of allNotes) { + let noteLabels: string[] = [] if (note.labels) { try { - const noteLabels: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] - const filteredLabels = noteLabels.filter( - l => l.toLowerCase() !== label.name.toLowerCase() - ) - - if (filteredLabels.length !== noteLabels.length) { - await prisma.note.update({ - where: { id: note.id }, - data: { - labels: (filteredLabels.length > 0 ? filteredLabels : null) as any - } - }) - } - } catch (e) { + const parsed = typeof note.labels === 'string' ? JSON.parse(note.labels) : note.labels + noteLabels = Array.isArray(parsed) + ? parsed.filter((n): n is string => typeof n === 'string') + : [] + } catch { + noteLabels = [] } } + + const filteredLabels = noteLabels.filter( + l => l.toLowerCase() !== label.name.toLowerCase() + ) + + const labelChanged = filteredLabels.length !== noteLabels.length + + if (labelChanged) { + await prisma.note.update({ + where: { id: note.id }, + data: { + labels: filteredLabels.length > 0 ? JSON.stringify(filteredLabels) : null, + labelRelations: { + disconnect: { id: label.id }, + }, + }, + }) + } } } diff --git a/memento-note/components/home-client.tsx b/memento-note/components/home-client.tsx index 801f8bf..47aadfe 100644 --- a/memento-note/components/home-client.tsx +++ b/memento-note/components/home-client.tsx @@ -140,6 +140,24 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { useReminderCheck(notes) + // Listen for global label deletion and immediately update local state + useEffect(() => { + const handler = (e: Event) => { + const { name } = (e as CustomEvent).detail + if (!name) return + const removeLabel = (note: Note) => { + const currentLabels = note.labels || [] + const updated = currentLabels.filter((l) => l.toLowerCase() !== name.toLowerCase()) + if (updated.length === currentLabels.length) return note + return { ...note, labels: updated.length > 0 ? updated : null } + } + setNotes((prev) => prev.map(removeLabel)) + setPinnedNotes((prev) => prev.map(removeLabel)) + } + window.addEventListener('label-deleted', handler) + return () => window.removeEventListener('label-deleted', handler) + }, []) + const prevRefreshKey = useRef(refreshKey) // Rechargement uniquement pour les filtres actifs (search, labels, notebook) diff --git a/memento-note/components/label-management-dialog.tsx b/memento-note/components/label-management-dialog.tsx index 4d38c68..58e2e05 100644 --- a/memento-note/components/label-management-dialog.tsx +++ b/memento-note/components/label-management-dialog.tsx @@ -50,8 +50,12 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) { const handleDeleteLabel = async (id: string) => { try { + const labelToDelete = labels.find(l => l.id === id) await deleteLabel(id) triggerRefresh() + if (labelToDelete) { + window.dispatchEvent(new CustomEvent('label-deleted', { detail: { name: labelToDelete.name } })) + } setConfirmDeleteId(null) } catch (error) { console.error('Failed to delete label:', error) diff --git a/memento-note/components/notes-tabs-view.tsx b/memento-note/components/notes-tabs-view.tsx index bc96741..4498511 100644 --- a/memento-note/components/notes-tabs-view.tsx +++ b/memento-note/components/notes-tabs-view.tsx @@ -325,6 +325,24 @@ export function NotesTabsView({ notes, onEdit, currentNotebookId }: NotesTabsVie ) }, [items]) + // Listen for global label deletion and immediately update local state + useEffect(() => { + const handler = (e: Event) => { + const { name } = (e as CustomEvent).detail + if (!name) return + setItems((prev) => + prev.map((note) => { + const currentLabels = note.labels || [] + const updated = currentLabels.filter((l) => l.toLowerCase() !== name.toLowerCase()) + if (updated.length === currentLabels.length) return note + return { ...note, labels: updated.length > 0 ? updated : null } + }) + ) + } + window.addEventListener('label-deleted', handler) + return () => window.removeEventListener('label-deleted', handler) + }, []) + // Scroll to top of sidebar on note change handled by NoteInlineEditor internally const sensors = useSensors( diff --git a/memento-note/lib/ai/services/auto-label-creation.service.ts b/memento-note/lib/ai/services/auto-label-creation.service.ts index 6862a73..3a6b2a6 100644 --- a/memento-note/lib/ai/services/auto-label-creation.service.ts +++ b/memento-note/lib/ai/services/auto-label-creation.service.ts @@ -431,14 +431,16 @@ Deine Antwort (nur JSON): ): Promise { let createdCount = 0 + await prisma.$transaction(async (tx) => { for (const suggestedLabel of suggestions.suggestedLabels) { if (!selectedLabels.includes(suggestedLabel.name)) continue - // Create the label - const label = await prisma.label.create({ - data: { + const label = await tx.label.upsert({ + where: { notebookId_name: { notebookId, name: suggestedLabel.name } as any }, + update: {}, + create: { name: suggestedLabel.name, - color: 'gray', // Default color, user can change later + color: 'gray', notebookId, userId, }, @@ -446,7 +448,7 @@ Deine Antwort (nur JSON): // Assign to notes: UI reads `Note.labels` (JSON string[]); relations must stay in sync for (const noteId of suggestedLabel.noteIds) { - const note = await prisma.note.findFirst({ + const note = await tx.note.findFirst({ where: { id: noteId, userId, notebookId }, select: { labels: true }, }) @@ -469,7 +471,7 @@ Deine Antwort (nur JSON): names = [...names, suggestedLabel.name] } - await prisma.note.update({ + await tx.note.update({ where: { id: noteId }, data: { labels: JSON.stringify(names), @@ -482,6 +484,7 @@ Deine Antwort (nur JSON): createdCount++ } + }) return createdCount }