feat: AI Overview recherche + AI Writer inline streaming
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m6s
CI / Deploy production (on server) (push) Successful in 2m2s

- 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:
Antigravity
2026-06-19 19:42:37 +00:00
parent 4750686b9f
commit a4238dc204
6 changed files with 311 additions and 124 deletions

View File

@@ -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 (