'use client' import { useMemo } from 'react' import { useInfiniteQuery, useMutation, useQuery, useQueryClient, type InfiniteData, type QueryClient, } from '@tanstack/react-query' import { appendContactToBookCache } from '../contact-book-cache' import { apiClient, ApiRequestError, OfflineError } from '../client' import { enqueue } from '../offline-queue' import type { ApiContact } from '../types' import type { ApiDiscoveryCounts, ApiDiscoveryScan, ApiDiscoveredProfile, ApiDiscoveredProfileGroup, ApiDispositionEmails, ApiEnrichmentSuggestion, ApiLLMModelsResponse, ApiLLMSettings, ApiSearchSettings, } from '@/lib/contacts/discovery-types' import { coerceDiscoveryGroups, dedupeDiscoveryGroups, discoveryGroupReactKey, parseDiscoveryOtherPage, parseDiscoveryOtherResponse, type DiscoveryOtherPageResult, } from '@/lib/contacts/discovery-grouping' import { filterVisibleEnrichmentSuggestions } from '@/lib/contacts/discovery-utils' import { useContactsList } from '@/lib/contacts/use-contacts-list' const ACTIVE_SCAN_STORAGE_KEY = 'ultimail-contact-discovery-scan-id' export function getPersistedScanId(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(ACTIVE_SCAN_STORAGE_KEY) } export function persistScanId(scanId: string | null) { if (typeof window === 'undefined') return if (scanId) { localStorage.setItem(ACTIVE_SCAN_STORAGE_KEY, scanId) } else { localStorage.removeItem(ACTIVE_SCAN_STORAGE_KEY) } } export function useDiscoveryCounts() { return useQuery({ queryKey: ['contact-discovery-counts'], queryFn: () => apiClient.get('/contacts/discovery/counts'), staleTime: 30_000, }) } export const OTHER_CONTACTS_PAGE_SIZE = 12 const OTHER_CONTACTS_QUERY_KEY = ['contact-discovery-other', 'groups', 'infinite'] as const function otherContactsQueryKey(searchQuery: string) { const q = searchQuery.trim() return q ? ([...OTHER_CONTACTS_QUERY_KEY, q] as const) : OTHER_CONTACTS_QUERY_KEY } /** Cache session si l'API renvoie encore toute la liste d'un coup (legacy). */ let legacyOtherContactsFullList: ApiDiscoveredProfileGroup[] | null = null function sliceLegacyOtherContactsPage(offset: number): DiscoveryOtherPageResult | null { if (!legacyOtherContactsFullList?.length) return null const groups = legacyOtherContactsFullList.slice(offset, offset + OTHER_CONTACTS_PAGE_SIZE) return { groups, total: legacyOtherContactsFullList.length, hasMore: offset + OTHER_CONTACTS_PAGE_SIZE < legacyOtherContactsFullList.length, } } export function flattenOtherContactPages( data: InfiniteData | undefined, ): ApiDiscoveredProfileGroup[] { if (!data?.pages?.length) return [] return dedupeDiscoveryGroups(data.pages.flatMap((page) => page.groups ?? [])) } function dedupeOtherContactsInfiniteCache( data: InfiniteData | undefined, ): InfiniteData | undefined { if (!data?.pages?.length) return data const seen = new Set() const pages = data.pages.map((page) => ({ ...page, groups: dedupeDiscoveryGroups(page.groups ?? []).filter((group) => { const key = discoveryGroupReactKey(group) if (seen.has(key)) return false seen.add(key) return true }), })) return { ...data, pages } } export function useOtherDiscoveredContacts(enabled = true, searchQuery = '') { const trimmedSearch = searchQuery.trim() return useInfiniteQuery({ queryKey: otherContactsQueryKey(trimmedSearch), queryFn: async ({ pageParam }) => { const offset = typeof pageParam === 'number' ? pageParam : 0 const cachedLegacy = offset > 0 && !trimmedSearch ? sliceLegacyOtherContactsPage(offset) : null if (cachedLegacy) { return { groups: coerceDiscoveryGroups(cachedLegacy.groups), total: cachedLegacy.total, hasMore: cachedLegacy.hasMore, } } const res = await apiClient.get<{ groups?: ApiDiscoveredProfileGroup[] profiles?: ApiDiscoveredProfile[] total?: number has_more?: boolean limit?: number offset?: number }>('/contacts/discovery/other', { limit: String(OTHER_CONTACTS_PAGE_SIZE), offset: String(offset), ...(trimmedSearch ? { q: trimmedSearch } : {}), }) const allGroups = coerceDiscoveryGroups(parseDiscoveryOtherResponse(res)) const serverPaginated = ('has_more' in res && res.has_more != null) || typeof res.total === 'number' if (!serverPaginated && allGroups.length > OTHER_CONTACTS_PAGE_SIZE) { legacyOtherContactsFullList = allGroups const groups = allGroups.slice(offset, offset + OTHER_CONTACTS_PAGE_SIZE) return { groups, total: allGroups.length, hasMore: offset + OTHER_CONTACTS_PAGE_SIZE < allGroups.length, } } legacyOtherContactsFullList = null const page = parseDiscoveryOtherPage(res, { offset, pageSize: OTHER_CONTACTS_PAGE_SIZE, }) return { groups: coerceDiscoveryGroups(page.groups ?? []), total: page.total, hasMore: page.hasMore, } }, initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { if (!lastPage?.hasMore) return undefined return allPages.reduce((sum, p) => sum + (p.groups?.length ?? 0), 0) }, staleTime: 30_000, enabled, refetchInterval: (query) => { const groups = flattenOtherContactPages(query.state.data) return groups.some((g) => g.profile?.enrichment_status === 'enriching') ? 2000 : false }, }) } export function useIgnoredDiscoveredContacts() { return useQuery({ queryKey: ['contact-discovery-ignored'], queryFn: async () => { const res = await apiClient.get<{ profiles: ApiDiscoveredProfile[] }>( '/contacts/discovery/ignored', ) return res.profiles ?? [] }, staleTime: 30_000, }) } export function useBlockedDiscoveredContacts() { return useQuery({ queryKey: ['contact-discovery-blocked'], queryFn: async () => { const res = await apiClient.get<{ profiles: ApiDiscoveredProfile[] }>( '/contacts/discovery/blocked', ) return res.profiles ?? [] }, staleTime: 30_000, }) } export function useEnrichmentSuggestions(type?: 'enrich' | 'all') { return useQuery({ queryKey: ['contact-discovery-suggestions', type], queryFn: async () => { const res = await apiClient.get<{ suggestions: ApiEnrichmentSuggestion[] }>( '/contacts/discovery/suggestions', type === 'enrich' ? { type: 'enrich' } : undefined, ) return res.suggestions ?? [] }, staleTime: 30_000, }) } /** Enrichment suggestions for existing contacts, excluding values already on the contact. */ export function useVisibleEnrichmentSuggestions() { const { contacts } = useContactsList() const query = useEnrichmentSuggestions('enrich') const suggestions = useMemo( () => filterVisibleEnrichmentSuggestions(query.data ?? [], contacts), [query.data, contacts], ) return { ...query, suggestions } } export function useActiveDiscoveryScan() { return useQuery({ queryKey: ['contact-discovery-scan-active'], queryFn: async () => { const res = await apiClient.get<{ scan: ApiDiscoveryScan | null }>( '/contacts/discovery/scan/active', ) const scan = res.scan ?? null if (scan && (scan.status === 'running' || scan.status === 'pending')) { persistScanId(scan.id) } else if (!scan) { persistScanId(null) } return scan }, refetchInterval: (query) => { const scan = query.state.data if (scan && (scan.status === 'running' || scan.status === 'pending')) { return 2000 } return false }, staleTime: 0, }) } export function useDiscoveryScan(scanId?: string) { return useQuery({ queryKey: ['contact-discovery-scan', scanId], queryFn: () => apiClient.get(`/contacts/discovery/scan/${scanId}`), enabled: !!scanId, refetchInterval: (query) => { const status = query.state.data?.status return status === 'running' || status === 'pending' ? 2000 : false }, }) } export function useStartDiscoveryScan() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (bookId?: string) => { const path = bookId ? `/contacts/discovery/scan?book_id=${encodeURIComponent(bookId)}` : '/contacts/discovery/scan' return apiClient.post(path) }, onSuccess: (scan) => { persistScanId(scan.id) queryClient.setQueryData(['contact-discovery-scan-active'], scan) queryClient.invalidateQueries({ queryKey: ['contact-discovery-scan', scan.id] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-counts'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-other'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-ignored'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-blocked'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-suggestions'] }) }, }) } export function useCancelDiscoveryScan() { const queryClient = useQueryClient() return useMutation({ mutationFn: () => apiClient.post('/contacts/discovery/scan/cancel'), onSuccess: () => { persistScanId(null) queryClient.setQueryData(['contact-discovery-scan-active'], null) queryClient.invalidateQueries({ queryKey: ['contact-discovery-scan-active'] }) }, }) } function groupMatchesProfileId(group: ApiDiscoveredProfileGroup, profileId: string): boolean { if (group.profile?.id === profileId) return true if (group.profile_ids?.includes(profileId)) return true return group.profiles?.some((p) => p.id === profileId) ?? false } function findOtherContactGroupInCache( queryClient: QueryClient, profileId: string, ): ApiDiscoveredProfileGroup | undefined { const data = queryClient.getQueryData>( OTHER_CONTACTS_QUERY_KEY, ) if (!data?.pages?.length) return undefined for (const page of data.pages) { for (const group of page.groups ?? []) { if (groupMatchesProfileId(group, profileId)) return group } } return undefined } /** Emails liés à un groupe découvert (pour blocage optimiste côté client). */ export function extractDiscoveryGroupEmails(group: ApiDiscoveredProfileGroup): string[] { const emails = new Set() if (group.primary_email) emails.add(group.primary_email.toLowerCase()) for (const profile of group.profiles ?? (group.profile ? [group.profile] : [])) { if (profile.primary_email) emails.add(profile.primary_email.toLowerCase()) for (const entry of profile.all_emails ?? []) { if (entry.email) emails.add(entry.email.toLowerCase()) } } return [...emails] } export function removeOtherContactGroupFromCache( queryClient: QueryClient, profileId: string, ): boolean { let removed = false queryClient.setQueryData>( OTHER_CONTACTS_QUERY_KEY, (old) => { if (!old) return old let didRemove = false const pages = old.pages.map((page) => { const groups = (page.groups ?? []).filter((group) => { if (groupMatchesProfileId(group, profileId)) { didRemove = true return false } return true }) return { ...page, groups } }) if (didRemove && pages[0]) { removed = true pages[0] = { ...pages[0], total: Math.max(0, pages[0].total - 1), } } return { ...old, pages } }, ) if (removed) { if (legacyOtherContactsFullList) { legacyOtherContactsFullList = legacyOtherContactsFullList.filter( (group) => !groupMatchesProfileId(group, profileId), ) } queryClient.setQueryData(['contact-discovery-counts'], (old) => { if (!old) return old return { ...old, other_contacts: Math.max(0, old.other_contacts - 1), } }) } return removed } interface OptimisticDispositionContext { previous: InfiniteData | undefined previousCounts: ApiDiscoveryCounts | undefined previousIgnored: ApiDiscoveredProfile[] | undefined previousBlocked: ApiDiscoveredProfile[] | undefined removedGroup: ApiDiscoveredProfileGroup | undefined } function rollbackDispositionContext( queryClient: QueryClient, context: OptimisticDispositionContext | undefined, ) { if (!context) return if (context.previous) { queryClient.setQueryData(OTHER_CONTACTS_QUERY_KEY, context.previous) } if (context.previousCounts) { queryClient.setQueryData(['contact-discovery-counts'], context.previousCounts) } if (context.previousIgnored) { queryClient.setQueryData(['contact-discovery-ignored'], context.previousIgnored) } if (context.previousBlocked) { queryClient.setQueryData(['contact-discovery-blocked'], context.previousBlocked) } } function optimisticRemoveFromDispositionLists( queryClient: QueryClient, profileId: string, ): Pick< OptimisticDispositionContext, 'previousIgnored' | 'previousBlocked' | 'previousCounts' > { const previousIgnored = queryClient.getQueryData([ 'contact-discovery-ignored', ]) const previousBlocked = queryClient.getQueryData([ 'contact-discovery-blocked', ]) const previousCounts = queryClient.getQueryData([ 'contact-discovery-counts', ]) let removedIgnored = false let removedBlocked = false queryClient.setQueryData(['contact-discovery-ignored'], (old) => { if (!old) return old const next = old.filter((p) => p.id !== profileId) removedIgnored = next.length !== old.length return next }) queryClient.setQueryData(['contact-discovery-blocked'], (old) => { if (!old) return old const next = old.filter((p) => p.id !== profileId) removedBlocked = next.length !== old.length return next }) if (removedIgnored || removedBlocked) { queryClient.setQueryData(['contact-discovery-counts'], (old) => { if (!old) return old return { ...old, ignored: removedIgnored ? Math.max(0, old.ignored - 1) : old.ignored, blocked: removedBlocked ? Math.max(0, old.blocked - 1) : old.blocked, } }) } return { previousIgnored, previousBlocked, previousCounts } } function optimisticRemoveOtherContact( queryClient: QueryClient, profileId: string, ): OptimisticDispositionContext { const previous = queryClient.getQueryData>( OTHER_CONTACTS_QUERY_KEY, ) const previousCounts = queryClient.getQueryData([ 'contact-discovery-counts', ]) const previousIgnored = queryClient.getQueryData([ 'contact-discovery-ignored', ]) const previousBlocked = queryClient.getQueryData([ 'contact-discovery-blocked', ]) const removedGroup = findOtherContactGroupInCache(queryClient, profileId) removeOtherContactGroupFromCache(queryClient, profileId) void queryClient.cancelQueries({ queryKey: OTHER_CONTACTS_QUERY_KEY }) return { previous, previousCounts, previousIgnored, previousBlocked, removedGroup, } } function prependIgnoredProfile( queryClient: QueryClient, group: ApiDiscoveredProfileGroup | undefined, ) { const profile = group?.profile ?? group?.profiles?.[0] if (!profile) return queryClient.setQueryData(['contact-discovery-ignored'], (old) => { const list = old ?? [] if (list.some((p) => p.id === profile.id)) return list return [{ ...profile, status: 'ignored' as const }, ...list] }) queryClient.setQueryData(['contact-discovery-counts'], (old) => { if (!old) return old return { ...old, ignored: old.ignored + 1 } }) } function prependBlockedProfile( queryClient: QueryClient, group: ApiDiscoveredProfileGroup | undefined, ) { const profile = group?.profile ?? group?.profiles?.[0] if (!profile) return queryClient.setQueryData(['contact-discovery-blocked'], (old) => { const list = old ?? [] if (list.some((p) => p.id === profile.id)) return list return [{ ...profile, status: 'blocked' as const }, ...list] }) queryClient.setQueryData(['contact-discovery-counts'], (old) => { if (!old) return old return { ...old, blocked: old.blocked + 1 } }) } export function useAcceptDiscoveredProfile() { const queryClient = useQueryClient() return useMutation({ mutationFn: (vars: { profileId: string; contactUid?: string }) => apiClient.post(`/contacts/discovery/profiles/${vars.profileId}/accept`, { contact_uid: vars.contactUid ?? '', }), onMutate: (vars) => { return optimisticRemoveOtherContact(queryClient, vars.profileId) }, onError: (_err, _vars, context) => { rollbackDispositionContext(queryClient, context) }, }) } /** Crée le contact NC puis marque le profil découvert comme accepté (retrait optimiste immédiat). */ export function useAddDiscoveredContact() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (vars: { bookId: string profileId: string contact: Partial }) => { try { const created = await apiClient.post( `/contacts/discovery/profiles/${vars.profileId}/add-to-book`, { book_id: vars.bookId, contact: vars.contact }, ) return { created, contactUid: created.uid } } catch (err) { if (!(err instanceof ApiRequestError) || (err.status !== 404 && err.status !== 405)) { throw err } const created = await apiClient.post( `/contacts/books/${vars.bookId}`, vars.contact, ) const contactUid = created?.uid ?? vars.contact.uid ?? '' await apiClient.post(`/contacts/discovery/profiles/${vars.profileId}/accept`, { contact_uid: contactUid, }) const merged: ApiContact = { ...(vars.contact as ApiContact), ...(created ?? {}), uid: contactUid, } return { created: merged, contactUid } } }, onMutate: (vars) => { return optimisticRemoveOtherContact(queryClient, vars.profileId) }, onSuccess: (data, vars) => { if (data.created.uid) { appendContactToBookCache(queryClient, vars.bookId, data.created) } }, onError: (err, _vars, context) => { rollbackDispositionContext(queryClient, context) if (err instanceof OfflineError) { enqueue({ id: crypto.randomUUID(), timestamp: Date.now(), type: 'create_contact', payload: { bookId: _vars.bookId, ..._vars.contact }, retries: 0, }) } }, }) } export function useRejectDiscoveredProfile() { const queryClient = useQueryClient() return useMutation({ mutationFn: (profileId: string) => apiClient.post(`/contacts/discovery/profiles/${profileId}/reject`), onMutate: (profileId) => { const dispositionContext = optimisticRemoveFromDispositionLists(queryClient, profileId) void queryClient.cancelQueries({ queryKey: ['contact-discovery-ignored'] }) void queryClient.cancelQueries({ queryKey: ['contact-discovery-blocked'] }) return dispositionContext }, onError: (_err, _profileId, context) => { if (!context) return if (context.previousIgnored) { queryClient.setQueryData(['contact-discovery-ignored'], context.previousIgnored) } if (context.previousBlocked) { queryClient.setQueryData(['contact-discovery-blocked'], context.previousBlocked) } if (context.previousCounts) { queryClient.setQueryData(['contact-discovery-counts'], context.previousCounts) } }, }) } export function useIgnoreDiscoveredProfile() { const queryClient = useQueryClient() return useMutation({ mutationFn: (profileId: string) => apiClient.post( `/contacts/discovery/profiles/${profileId}/ignore`, ), onMutate: (profileId) => { const context = optimisticRemoveOtherContact(queryClient, profileId) prependIgnoredProfile(queryClient, context.removedGroup) return context }, onError: (_err, _profileId, context) => { rollbackDispositionContext(queryClient, context) }, }) } export function useBlockDiscoveredProfile() { const queryClient = useQueryClient() return useMutation({ mutationFn: (profileId: string) => apiClient.post( `/contacts/discovery/profiles/${profileId}/block`, ), onMutate: (profileId) => { const context = optimisticRemoveOtherContact(queryClient, profileId) prependBlockedProfile(queryClient, context.removedGroup) return context }, onError: (_err, _profileId, context) => { rollbackDispositionContext(queryClient, context) }, }) } export interface ProfileEnrichResponse { profile_id: string enrichment_status: ApiDiscoveredProfile['enrichment_status'] } function patchGroupEnrichmentStatus( groups: ApiDiscoveredProfileGroup[] | undefined, profileId: string, status: ApiDiscoveredProfile['enrichment_status'], ): ApiDiscoveredProfileGroup[] | undefined { if (!groups) return groups return groups.map((g) => { const id = g.profile?.id ?? g.profile_ids[0] if (id !== profileId) return g const patchProfile = (p: ApiDiscoveredProfile): ApiDiscoveredProfile => ({ ...p, enrichment_status: status, }) return { ...g, profile: g.profile ? patchProfile(g.profile) : g.profile, profiles: g.profiles?.map(patchProfile), } }) } export function useEnrichDiscoveredProfile() { const queryClient = useQueryClient() return useMutation({ mutationFn: (profileId: string) => apiClient.post( `/contacts/discovery/profiles/${profileId}/enrich`, ), onMutate: async (profileId) => { await queryClient.cancelQueries({ queryKey: OTHER_CONTACTS_QUERY_KEY }) queryClient.setQueryData>( OTHER_CONTACTS_QUERY_KEY, (old) => { if (!old) return old return { ...old, pages: old.pages.map((page) => ({ ...page, groups: patchGroupEnrichmentStatus(page.groups ?? [], profileId, 'enriching') ?? page.groups ?? [], })), } }, ) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['contact-discovery-other'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-suggestions'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-counts'] }) }, }) } export function useAcceptEnrichmentSuggestion() { const queryClient = useQueryClient() return useMutation({ mutationFn: (suggestionId: string) => apiClient.post( `/contacts/discovery/suggestions/${suggestionId}/accept`, ), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contact-discovery-counts'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-suggestions'] }) queryClient.invalidateQueries({ queryKey: ['contacts'] }) }, }) } export function useRejectEnrichmentSuggestion() { const queryClient = useQueryClient() return useMutation({ mutationFn: (suggestionId: string) => apiClient.post(`/contacts/discovery/suggestions/${suggestionId}/reject`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contact-discovery-counts'] }) queryClient.invalidateQueries({ queryKey: ['contact-discovery-suggestions'] }) }, }) } function normalizeLLMSettings(raw: ApiLLMSettings): ApiLLMSettings { return { ...raw, default_provider_id: raw.default_provider_id ?? '', providers: raw.providers ?? [], } } export function useLLMSettings() { return useQuery({ queryKey: ['llm-settings'], queryFn: async () => { const data = await apiClient.get('/contacts/discovery/llm-settings') return normalizeLLMSettings(data) }, staleTime: 60_000, }) } export function useUpdateLLMSettings() { const queryClient = useQueryClient() return useMutation({ mutationFn: (settings: ApiLLMSettings) => apiClient.put('/contacts/discovery/llm-settings', settings), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['llm-settings'] }) }, }) } function normalizeSearchSettings(raw: ApiSearchSettings): ApiSearchSettings { return { ...raw, default_provider_id: raw.default_provider_id ?? '', providers: raw.providers ?? [], } } export function useSearchSettings() { return useQuery({ queryKey: ['search-settings'], queryFn: async () => { const data = await apiClient.get('/contacts/discovery/search-settings') return normalizeSearchSettings(data) }, staleTime: 60_000, }) } export function useUpdateSearchSettings() { const queryClient = useQueryClient() return useMutation({ mutationFn: (settings: ApiSearchSettings) => apiClient.put('/contacts/discovery/search-settings', settings), onSuccess: (data) => { queryClient.setQueryData(['search-settings'], normalizeSearchSettings(data)) }, }) } export function useDiscoverLLMModels(baseUrl: string, apiKey: string) { const trimmedBaseUrl = baseUrl.trim() const trimmedApiKey = apiKey.trim() return useQuery({ queryKey: ['llm-models', trimmedBaseUrl, trimmedApiKey], queryFn: () => apiClient.post('/contacts/discovery/llm-models/discover', { base_url: trimmedBaseUrl, api_key: trimmedApiKey || undefined, }), enabled: trimmedBaseUrl.length > 0, staleTime: 5 * 60_000, retry: false, }) } export function scanProgressLabel(scan: ApiDiscoveryScan): string { const phase = scanPhaseLabel(scan.phase) if (scan.phase === 'enriching') { const done = scan.profiles_found const total = scan.profiles_total if (total > 0) { return `${phase} ${done.toLocaleString('fr-FR')} / ${total.toLocaleString('fr-FR')} contacts` } return phase } if (scan.phase === 'building_profiles') { const done = scan.profiles_found const total = scan.profiles_total if (total > 0) { return `${phase} ${done.toLocaleString('fr-FR')} / ${total.toLocaleString('fr-FR')} profils` } return phase } if (scan.phase === 'scanning_messages') { if (scan.total_messages > 0) { return `${phase} ${scan.messages_scanned.toLocaleString('fr-FR')} / ${scan.total_messages.toLocaleString('fr-FR')} messages` } return `${phase} ${scan.messages_scanned.toLocaleString('fr-FR')} messages` } return phase } export function scanPhaseLabel(phase: ApiDiscoveryScan['phase']): string { switch (phase) { case 'scanning_messages': return 'Analyse des messages…' case 'building_profiles': return 'Construction des profils…' case 'enriching': return 'Enrichissement IA…' case 'done': return 'Terminé' default: return 'En attente…' } }