feat: AI Overview recherche + AI Writer inline streaming
- AI Overview : synthèse IA en haut des résultats de recherche (Ctrl+K) - Service search-overview.service.ts - Endpoint /api/ai/search-overview - Carte 'Réponse IA' avec Sparkles en haut du panneau gauche - Pur additif, ne modifie pas le classement des résultats - AI Writer inline : slash menu → 'Écrire avec l'IA' → champ inline - Mode 'write' dans paragraph-refactor.service.ts - Streaming paragraphe par paragraphe (120ms delay) - Nettoyage HTML (espaces vides supprimés) - Ref synchrone pour éviter fermeture du menu pendant la frappe - Fix: index slashCommands AI Writer corrigé - Fix: Loader2 import manquant - i18n FR/EN
This commit is contained in:
@@ -12,6 +12,8 @@ import {
|
||||
HelpCircle,
|
||||
Command,
|
||||
FileText,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
@@ -64,6 +66,8 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
const [savedQueries, setSavedQueries] = useState<string[]>([])
|
||||
const [results, setResults] = useState<Note[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [overview, setOverview] = useState<string | null>(null)
|
||||
const [overviewLoading, setOverviewLoading] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -137,6 +141,36 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
}
|
||||
}, [query, searchInTrash])
|
||||
|
||||
// Fetch AI overview after results load
|
||||
useEffect(() => {
|
||||
if (!query.trim() || results.length === 0) {
|
||||
setOverview(null)
|
||||
return
|
||||
}
|
||||
setOverviewLoading(true)
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const topResults = results.slice(0, 5).map(r => ({
|
||||
title: r.title || 'Sans titre',
|
||||
snippet: r.content?.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400) || '',
|
||||
}))
|
||||
const res = await fetch('/api/ai/search-overview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: query.trim(), results: topResults }),
|
||||
})
|
||||
const data = await res.json()
|
||||
setOverview(data.hasRelevantInfo ? data.answer : null)
|
||||
} catch {
|
||||
setOverview(null)
|
||||
} finally {
|
||||
setOverviewLoading(false)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [query, results])
|
||||
|
||||
// Reset selected index when results change
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
@@ -501,6 +535,25 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
{/* Left — results list */}
|
||||
<div className="w-[45%] h-full border-r border-border/40 dark:border-zinc-800 flex flex-col bg-[#FAF9F5]/30 dark:bg-[#121212]/30 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
{/* AI Overview */}
|
||||
{(overviewLoading || overview) && (
|
||||
<div className="mb-2 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.03] p-3" dir="auto">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Sparkles size={12} className="text-brand-accent" />
|
||||
<span className="text-[9px] font-bold uppercase tracking-widest text-brand-accent">
|
||||
{overviewLoading ? 'Analyse...' : 'Réponse IA'}
|
||||
</span>
|
||||
</div>
|
||||
{overviewLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 size={12} className="animate-spin text-brand-accent" />
|
||||
<span className="text-[11px] text-muted-foreground">Synthèse des résultats...</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[12px] leading-relaxed text-foreground/80">{overview}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{filteredMatches.map((m, idx) => {
|
||||
const isSelected = idx === selectedIndex
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user