Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
||
import { useDebounce } from './use-debounce'
|
||
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||
import { countPlainWords, MIN_WORDS_FOR_AUTO_TITLE } from '@/lib/text/plain-text'
|
||
|
||
export interface TitleSuggestion {
|
||
title: string
|
||
confidence: number
|
||
reasoning?: string
|
||
}
|
||
|
||
interface UseTitleSuggestionsProps {
|
||
content: string
|
||
enabled?: boolean
|
||
}
|
||
|
||
export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggestionsProps) {
|
||
const { requestAiConsent, hasAiConsent } = useAiConsent()
|
||
const [suggestions, setSuggestions] = useState<TitleSuggestion[]>([])
|
||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const abortRef = useRef<AbortController | null>(null)
|
||
|
||
// Debounce content by 2s to avoid excessive API calls
|
||
const debouncedContent = useDebounce(content, 2000)
|
||
|
||
useEffect(() => {
|
||
if (!enabled || !debouncedContent) {
|
||
setSuggestions([])
|
||
return
|
||
}
|
||
|
||
// Compter le texte visible, pas les balises de l’éditeur
|
||
if (countPlainWords(debouncedContent) < MIN_WORDS_FOR_AUTO_TITLE) {
|
||
setSuggestions([])
|
||
return
|
||
}
|
||
|
||
// Cancel previous request
|
||
abortRef.current?.abort()
|
||
const controller = new AbortController()
|
||
abortRef.current = controller
|
||
|
||
const generateTitles = async () => {
|
||
if (!hasAiConsent) {
|
||
const consented = await requestAiConsent()
|
||
if (!consented) {
|
||
setSuggestions([])
|
||
return
|
||
}
|
||
}
|
||
|
||
setIsAnalyzing(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const response = await fetch('/api/ai/title-suggestions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content: debouncedContent }),
|
||
signal: controller.signal,
|
||
})
|
||
|
||
if (controller.signal.aborted) return
|
||
|
||
if (!response.ok) {
|
||
setSuggestions([])
|
||
if (response.status !== 400) {
|
||
setError('Failed to generate title suggestions')
|
||
}
|
||
return
|
||
}
|
||
|
||
const data = await response.json()
|
||
setSuggestions(data.suggestions || [])
|
||
} catch (err: any) {
|
||
if (err.name === 'AbortError') return
|
||
console.error('Title suggestions error:', err)
|
||
setSuggestions([])
|
||
setError('Failed to generate title suggestions')
|
||
} finally {
|
||
if (!controller.signal.aborted) {
|
||
setIsAnalyzing(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
generateTitles()
|
||
}, [debouncedContent, enabled, hasAiConsent, requestAiConsent])
|
||
|
||
// Cleanup on unmount
|
||
useEffect(() => {
|
||
return () => { abortRef.current?.abort(); };
|
||
}, []);
|
||
|
||
return {
|
||
suggestions,
|
||
isAnalyzing,
|
||
error,
|
||
clearSuggestions: () => setSuggestions([])
|
||
}
|
||
}
|