Files
Momento/memento-note/components/auto-label-suggestion-dialog.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

246 lines
8.2 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog'
import { Sparkles, CheckCircle2, Loader2, Tag } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import type { AutoLabelSuggestion, SuggestedLabel } from '@/lib/ai/services'
import { cn } from '@/lib/utils'
interface AutoLabelSuggestionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
notebookId: string | null
onLabelsCreated: () => void
}
export function AutoLabelSuggestionDialog({
open,
onOpenChange,
notebookId,
onLabelsCreated,
}: AutoLabelSuggestionDialogProps) {
const { t } = useLanguage()
const { requestAiConsent } = useAiConsent()
const [suggestions, setSuggestions] = useState<AutoLabelSuggestion | null>(null)
const [loading, setLoading] = useState(false)
const [creating, setCreating] = useState(false)
const [selectedLabels, setSelectedLabels] = useState<Set<string>>(new Set())
useEffect(() => {
if (open && notebookId) {
fetchSuggestions()
} else {
setSuggestions(null)
setSelectedLabels(new Set())
}
}, [open, notebookId])
const fetchSuggestions = async () => {
if (!notebookId) return
const consented = await requestAiConsent()
if (!consented) return
setLoading(true)
try {
const response = await fetch('/api/ai/auto-labels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
notebookId,
language: document.documentElement.lang || 'en',
}),
})
const data = await response.json()
if (data.success && data.data) {
setSuggestions(data.data)
const allLabelNames = new Set<string>(data.data.suggestedLabels.map((l: SuggestedLabel) => l.name as string))
setSelectedLabels(allLabelNames)
} else {
if (data.message) {
toast.info(data.message)
} else {
toast.info(t('ai.autoLabels.noSuggestions') || 'Pas assez de notes pour générer des labels (minimum 15)')
}
onOpenChange(false)
}
} catch (error) {
console.error('Failed to fetch label suggestions:', error)
toast.error(t('ai.autoLabels.error'))
onOpenChange(false)
} finally {
setLoading(false)
}
}
const toggleLabelSelection = (labelName: string) => {
const newSelected = new Set(selectedLabels)
if (newSelected.has(labelName)) {
newSelected.delete(labelName)
} else {
newSelected.add(labelName)
}
setSelectedLabels(newSelected)
}
const handleCreateLabels = async () => {
if (!suggestions || selectedLabels.size === 0) {
toast.error(t('ai.autoLabels.noLabelsSelected'))
return
}
setCreating(true)
try {
const response = await fetch('/api/ai/auto-labels', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
suggestions,
selectedLabels: Array.from(selectedLabels),
}),
})
const data = await response.json()
if (data.success) {
toast.success(t('ai.autoLabels.created', { count: data.data.createdCount }))
onLabelsCreated()
onOpenChange(false)
} else {
toast.error(data.error || t('ai.autoLabels.error'))
}
} catch (error) {
console.error('Failed to create labels:', error)
toast.error(t('ai.autoLabels.error'))
} finally {
setCreating(false)
}
}
if (loading) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="sr-only">{t('ai.autoLabels.analyzing')}</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center justify-center py-12">
<div className="w-16 h-16 rounded-full border border-dashed border-brand-accent/20 flex items-center justify-center mb-4">
<Loader2 className="h-6 w-6 animate-spin text-brand-accent" />
</div>
<p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
{t('ai.autoLabels.analyzing')}
</p>
</div>
</DialogContent>
</Dialog>
)
}
if (!suggestions) {
return null
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand-accent" />
{t('ai.autoLabels.title')}
</DialogTitle>
<DialogDescription>
{t('ai.autoLabels.description', {
notebook: suggestions.notebookName,
count: suggestions.totalNotes,
})}
</DialogDescription>
</DialogHeader>
<div className="space-y-2 py-4">
{suggestions.suggestedLabels.map((label) => {
const isSelected = selectedLabels.has(label.name)
return (
<div
key={label.name}
className={cn(
"flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all",
isSelected
? "bg-brand-accent/5 border-brand-accent/30 hover:bg-brand-accent/10"
: "border-border hover:bg-muted/50"
)}
onClick={() => toggleLabelSelection(label.name)}
>
<div className={cn(
"w-5 h-5 rounded-full border-2 flex items-center justify-center mt-0.5 transition-all shrink-0",
isSelected
? "bg-brand-accent border-brand-accent"
: "border-border"
)}>
{isSelected && <CheckCircle2 className="h-3.5 w-3.5 text-white" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Tag className="h-3.5 w-3.5 text-brand-accent/60" />
<span className="font-medium text-sm">{label.name}</span>
<Sparkles className="h-3 w-3 text-brand-accent/40" />
</div>
<div className="flex items-center gap-3 mt-1.5">
<span className="text-[10px] text-muted-foreground">
{t('ai.autoLabels.notesCount', { count: label.count })}
</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-brand-accent/10 text-brand-accent font-bold">
{Math.round(label.confidence * 100)}%
</span>
</div>
</div>
</div>
)
})}
</div>
<DialogFooter className="gap-2">
<button
onClick={() => onOpenChange(false)}
disabled={creating}
className="flex-1 py-3 border border-border rounded-xl text-[10px] font-bold uppercase tracking-widest hover:bg-muted transition-all"
>
{t('general.cancel')}
</button>
<button
onClick={handleCreateLabels}
disabled={selectedLabels.size === 0 || creating}
className="flex-1 py-3 bg-brand-accent text-white rounded-xl text-[10px] font-bold uppercase tracking-widest hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 disabled:opacity-50"
>
{creating ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{t('ai.autoLabels.creating')}
</span>
) : (
<span className="flex items-center justify-center gap-2">
<CheckCircle2 className="h-4 w-4" />
{t('ai.autoLabels.create')}
</span>
)}
</button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}