feat: IA Note — rename panel, add Resource tab + chat hover-actions
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
- Renamed 'AI Copilot' / 'Assistant IA' → 'IA Note' everywhere in UI - Added 3rd 'Ressource' tab in IA Note panel with: * Optional URL field that scrapes page text (via new scrapePageText action) * Textarea for paste (markdown, HTML, plain text) * 3 integration modes: Remplacer / Compléter (AI) / Fusionner (AI) * Dual-format preview: Rendu + Markdown brut before applying - Added hover-actions on assistant chat messages: * Remplacer / Compléter / Fusionner appear on hover * Triggers same preview/apply flow via resource tab - New API route: POST /api/ai/enrich-from-resource * Supports complete and merge modes with language-aware prompts - Extended scrape.ts with scrapePageText() (full content extraction)
This commit is contained in:
@@ -11,7 +11,8 @@ import {
|
||||
Briefcase, Palette, GraduationCap, Coffee,
|
||||
Lightbulb, Minimize2, AlignLeft, Wand2,
|
||||
Globe, BookOpen, FileText, RotateCcw, Check,
|
||||
Maximize2, ImageIcon,
|
||||
Maximize2, ImageIcon, Link2, Download, ArrowDownToLine,
|
||||
GitMerge, PlusCircle, Eye, Code,
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { MarkdownContent } from '@/components/markdown-content'
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { getNotebookIcon } from '@/lib/notebook-icon'
|
||||
import { scrapePageText } from '@/app/actions/scrape'
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -101,10 +103,10 @@ export function ContextualAIChat({
|
||||
const { t, language } = useLanguage()
|
||||
const webSearchAvailable = useWebSearchAvailable()
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'actions'>('chat')
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'actions' | 'resource'>('chat')
|
||||
const [selectedTone, setSelectedTone] = useState('professional')
|
||||
const [input, setInput] = useState('')
|
||||
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note') // 'note', 'all', or notebook ID
|
||||
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note')
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
@@ -112,6 +114,17 @@ export function ContextualAIChat({
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
||||
|
||||
// Resource tab state
|
||||
const [resourceUrl, setResourceUrl] = useState('')
|
||||
const [resourceText, setResourceText] = useState('')
|
||||
const [resourceScraping, setResourceScraping] = useState(false)
|
||||
const [resourceMode, setResourceMode] = useState<'replace' | 'complete' | 'merge'>('complete')
|
||||
const [resourcePreview, setResourcePreview] = useState<{ text: string; source: string } | null>(null)
|
||||
const [resourceEnriching, setResourceEnriching] = useState(false)
|
||||
const [resourcePreviewFormat, setResourcePreviewFormat] = useState<'rendered' | 'markdown'>('rendered')
|
||||
// hoveredMsgId: which chat message shows inject actions
|
||||
const [hoveredMsgId, setHoveredMsgId] = useState<string | null>(null)
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current
|
||||
@@ -230,12 +243,102 @@ export function ContextualAIChat({
|
||||
|
||||
const handleDiscardPreview = () => setActionPreview(null)
|
||||
|
||||
// ── Resource tab handlers ────────────────────────────────────────────────────
|
||||
|
||||
const handleScrapeUrl = async () => {
|
||||
if (!resourceUrl.trim()) return
|
||||
setResourceScraping(true)
|
||||
try {
|
||||
const result = await scrapePageText(resourceUrl.trim())
|
||||
if (!result) { toast.error('Impossible de charger cette URL'); return }
|
||||
setResourceText(result.text)
|
||||
toast.success(`Page chargée : ${result.title.slice(0, 40)}`)
|
||||
} catch {
|
||||
toast.error('Erreur lors du chargement de la page')
|
||||
} finally {
|
||||
setResourceScraping(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResourcePreview = async () => {
|
||||
if (!resourceText.trim()) { toast.error('Collez du texte ou chargez une URL d\'abord'); return }
|
||||
if (resourceMode === 'replace') {
|
||||
setResourcePreview({ text: resourceText, source: 'paste' })
|
||||
return
|
||||
}
|
||||
setResourceEnriching(true)
|
||||
try {
|
||||
const res = await fetch('/api/ai/enrich-from-resource', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
existingContent: noteContent || '',
|
||||
resourceText: resourceText.trim(),
|
||||
mode: resourceMode,
|
||||
language,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || 'Erreur IA')
|
||||
setResourcePreview({ text: data.enrichedContent, source: resourceMode })
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || 'Erreur lors de l\'enrichissement')
|
||||
} finally {
|
||||
setResourceEnriching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplyResourcePreview = () => {
|
||||
if (!resourcePreview || !onApplyToNote) return
|
||||
onApplyToNote(resourcePreview.text)
|
||||
setResourcePreview(null)
|
||||
setResourceText('')
|
||||
setResourceUrl('')
|
||||
toast.success('Contenu appliqué à la note ✓')
|
||||
}
|
||||
|
||||
/** Called from chat hover-actions: inject a chat message into the note */
|
||||
const handleInjectFromChat = async (msgText: string, mode: 'replace' | 'complete' | 'merge') => {
|
||||
if (mode === 'replace') {
|
||||
setResourceText(msgText)
|
||||
setResourceMode('replace')
|
||||
setResourcePreview({ text: msgText, source: 'chat' })
|
||||
setActiveTab('resource')
|
||||
return
|
||||
}
|
||||
setResourceText(msgText)
|
||||
setResourceMode(mode)
|
||||
setActiveTab('resource')
|
||||
// Auto-launch enrichment
|
||||
setResourceEnriching(true)
|
||||
try {
|
||||
const res = await fetch('/api/ai/enrich-from-resource', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
existingContent: noteContent || '',
|
||||
resourceText: msgText,
|
||||
mode,
|
||||
language,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || 'Erreur IA')
|
||||
setResourcePreview({ text: data.enrichedContent, source: mode })
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || 'Erreur enrichissement')
|
||||
} finally {
|
||||
setResourceEnriching(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scope label ─────────────────────────────────────────────────────────────
|
||||
const scopeLabel =
|
||||
chatScope === 'note' ? t('ai.thisNote')
|
||||
: chatScope === 'all' ? t('ai.allMyNotes')
|
||||
: notebooks.find(n => n.id === chatScope)?.name ?? t('ai.notebookGeneric')
|
||||
|
||||
|
||||
return (
|
||||
<aside className={cn(
|
||||
'border-l border-border/40 bg-card flex flex-col self-stretch flex-shrink-0 z-10 transition-all duration-300',
|
||||
@@ -248,7 +351,7 @@ export function ContextualAIChat({
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-semibold text-foreground flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary shrink-0" />
|
||||
{t('ai.assistantTitle')}
|
||||
IA Note
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{noteTitle ? `"${noteTitle}"` : t('ai.currentNote')}
|
||||
@@ -284,20 +387,23 @@ export function ContextualAIChat({
|
||||
|
||||
{/* ── Tabs ─────────────────────────────────────────────────── */}
|
||||
<div className="flex border-b border-border/40 shrink-0">
|
||||
{(['chat', 'actions'] as const).map(tab => (
|
||||
{([
|
||||
{ id: 'chat', icon: Bot, label: t('ai.chatTab') },
|
||||
{ id: 'actions', icon: Wand2, label: t('ai.noteActions') },
|
||||
{ id: 'resource', icon: ArrowDownToLine, label: 'Ressource' },
|
||||
] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={cn(
|
||||
'flex-1 py-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all capitalize',
|
||||
activeTab === tab
|
||||
'flex-1 py-2.5 border-b-2 text-xs font-semibold flex items-center justify-center gap-1.5 transition-all',
|
||||
activeTab === tab.id
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{tab === 'chat' && <Bot className="h-3.5 w-3.5" />}
|
||||
{tab === 'actions' && <Wand2 className="h-3.5 w-3.5" />}
|
||||
{tab === 'chat' ? t('ai.chatTab') : t('ai.noteActions')}
|
||||
<tab.icon className="h-3 w-3" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -322,32 +428,70 @@ export function ContextualAIChat({
|
||||
|
||||
{messages.map((msg: UIMessage) => {
|
||||
const content = getMessageContent(msg)
|
||||
// Skip empty assistant messages (thinking/reasoning phase)
|
||||
if (msg.role === 'assistant' && !content) return null
|
||||
const isAssistant = msg.role === 'assistant'
|
||||
const isHovered = hoveredMsgId === msg.id
|
||||
return (
|
||||
<div key={msg.id} className={cn('flex gap-2', msg.role === 'user' && 'flex-row-reverse')}>
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn('flex gap-2', !isAssistant && 'flex-row-reverse')}
|
||||
onMouseEnter={() => isAssistant && setHoveredMsgId(msg.id)}
|
||||
onMouseLeave={() => setHoveredMsgId(null)}
|
||||
>
|
||||
<div className={cn(
|
||||
'w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 border text-[10px] font-bold',
|
||||
msg.role === 'user'
|
||||
!isAssistant
|
||||
? 'bg-slate-100 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300'
|
||||
: 'bg-primary/10 text-primary border-primary/20',
|
||||
)}>
|
||||
{msg.role === 'user' ? 'U' : <Bot className="h-3 w-3" />}
|
||||
{!isAssistant ? 'U' : <Bot className="h-3 w-3" />}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'max-w-[88%] p-3 rounded-2xl text-sm leading-relaxed',
|
||||
msg.role === 'user'
|
||||
? 'bg-primary text-primary-foreground rounded-tr-sm'
|
||||
: 'bg-muted/40 border border-border/40 rounded-tl-sm text-foreground',
|
||||
)}>
|
||||
{msg.role === 'assistant'
|
||||
? <MarkdownContent content={content} />
|
||||
: <p>{content}</p>}
|
||||
<div className="flex flex-col gap-1 max-w-[88%]">
|
||||
<div className={cn(
|
||||
'p-3 rounded-2xl text-sm leading-relaxed',
|
||||
!isAssistant
|
||||
? 'bg-primary text-primary-foreground rounded-tr-sm'
|
||||
: 'bg-muted/40 border border-border/40 rounded-tl-sm text-foreground',
|
||||
)}>
|
||||
{isAssistant
|
||||
? <MarkdownContent content={content} />
|
||||
: <p>{content}</p>}
|
||||
</div>
|
||||
{/* Hover-actions — visible only on assistant messages */}
|
||||
{isAssistant && onApplyToNote && (
|
||||
<div className={cn(
|
||||
'flex gap-1 transition-all duration-150',
|
||||
isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-1 pointer-events-none',
|
||||
)}>
|
||||
<button
|
||||
onClick={() => handleInjectFromChat(content, 'replace')}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-primary/10 hover:text-primary border border-border/40 transition-colors"
|
||||
title="Remplacer le contenu de la note par ce message"
|
||||
>
|
||||
<Download className="h-2.5 w-2.5" /> Remplacer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectFromChat(content, 'complete')}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-emerald-50 hover:text-emerald-600 dark:hover:bg-emerald-950/30 border border-border/40 transition-colors"
|
||||
title="Compléter la note avec ce message (IA)"
|
||||
>
|
||||
<PlusCircle className="h-2.5 w-2.5" /> Compléter
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectFromChat(content, 'merge')}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-violet-50 hover:text-violet-600 dark:hover:bg-violet-950/30 border border-border/40 transition-colors"
|
||||
title="Fusionner avec la note (IA)"
|
||||
>
|
||||
<GitMerge className="h-2.5 w-2.5" /> Fusionner
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 border border-primary/20">
|
||||
@@ -603,6 +747,197 @@ export function ContextualAIChat({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══════════════════════════════════════════════════════════ */}
|
||||
{/* ── TAB: RESSOURCE ──────────────────────────────────── */}
|
||||
{/* ══════════════════════════════════════════════════════════ */}
|
||||
{activeTab === 'resource' && (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
{/* Preview panel */}
|
||||
{resourcePreview ? (
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* Preview header */}
|
||||
<div className="px-4 py-2.5 border-b border-border/40 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-semibold text-foreground">
|
||||
{resourcePreview.source === 'chat' ? '💬 Depuis le chat'
|
||||
: resourcePreview.source === 'replace' ? '↓ Remplacement'
|
||||
: resourcePreview.source === 'complete' ? '✦ Complété par IA'
|
||||
: '⟳ Fusionné par IA'}
|
||||
</p>
|
||||
{/* Format toggle */}
|
||||
<div className="flex rounded-md border border-border/40 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setResourcePreviewFormat('rendered')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 text-[10px] font-medium transition-colors',
|
||||
resourcePreviewFormat === 'rendered'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-card text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Eye className="h-2.5 w-2.5" /> Rendu
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResourcePreviewFormat('markdown')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 text-[10px] font-medium transition-colors',
|
||||
resourcePreviewFormat === 'markdown'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-card text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Code className="h-2.5 w-2.5" /> Markdown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setResourcePreview(null)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Preview content */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{resourcePreviewFormat === 'rendered' ? (
|
||||
<div className="text-sm leading-relaxed text-foreground bg-muted/20 border border-border/30 rounded-xl p-3">
|
||||
<MarkdownContent content={resourcePreview.text} />
|
||||
</div>
|
||||
) : (
|
||||
<pre className="text-xs leading-relaxed text-foreground bg-muted/20 border border-border/30 rounded-xl p-3 whitespace-pre-wrap font-mono overflow-x-auto">
|
||||
{resourcePreview.text}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
{/* Apply / Discard */}
|
||||
<div className="p-3 border-t border-border/40 flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
className="flex-1 text-xs gap-1.5"
|
||||
onClick={() => setResourcePreview(null)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" /> Annuler
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 text-xs gap-1.5 bg-primary"
|
||||
onClick={handleApplyResourcePreview}
|
||||
disabled={!onApplyToNote}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" /> Appliquer à la note
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="p-3 space-y-3">
|
||||
{/* URL loader */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
|
||||
<Link2 className="h-3 w-3 inline mr-1" />URL (optionnel)
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
type="url"
|
||||
value={resourceUrl}
|
||||
onChange={e => setResourceUrl(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleScrapeUrl()}
|
||||
placeholder="https://..."
|
||||
className="flex-1 h-8 px-2.5 text-xs bg-card border border-border/60 rounded-lg focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-xs shrink-0"
|
||||
onClick={handleScrapeUrl}
|
||||
disabled={resourceScraping || !resourceUrl.trim()}
|
||||
>
|
||||
{resourceScraping
|
||||
? <Loader2 className="h-3 w-3 animate-spin" />
|
||||
: <Download className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text area */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
|
||||
<FileText className="h-3 w-3 inline mr-1" />Texte de la ressource
|
||||
</label>
|
||||
<textarea
|
||||
value={resourceText}
|
||||
onChange={e => setResourceText(e.target.value)}
|
||||
placeholder="Collez votre texte ici (markdown, HTML, texte brut…)"
|
||||
rows={8}
|
||||
className="w-full px-2.5 py-2 text-xs bg-card border border-border/60 rounded-lg focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 resize-none transition-all font-mono leading-relaxed"
|
||||
/>
|
||||
{resourceText && (
|
||||
<p className="text-[9px] text-muted-foreground mt-0.5 text-right">
|
||||
{resourceText.split(/\s+/).filter(Boolean).length} mots
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mode selector */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
|
||||
Mode d'intégration
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{([
|
||||
{ id: 'replace', label: 'Remplacer', desc: 'Direct, sans IA', icon: Download, color: 'primary' },
|
||||
{ id: 'complete', label: 'Compléter', desc: 'Ajoute sans réécrire', icon: PlusCircle, color: 'emerald' },
|
||||
{ id: 'merge', label: 'Fusionner', desc: 'Réécrit et intègre', icon: GitMerge, color: 'violet' },
|
||||
] as const).map(m => {
|
||||
const Icon = m.icon
|
||||
const sel = resourceMode === m.id
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => setResourceMode(m.id)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-1 py-2 px-1 rounded-lg border text-center transition-all',
|
||||
sel
|
||||
? m.color === 'primary'
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: m.color === 'emerald'
|
||||
? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30'
|
||||
: 'border-violet-500 bg-violet-50 text-violet-700 dark:bg-violet-950/30'
|
||||
: 'border-border/40 text-muted-foreground bg-card hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px] font-semibold leading-tight">{m.label}</span>
|
||||
<span className="text-[9px] leading-tight opacity-70">{m.desc}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview button */}
|
||||
<Button
|
||||
className="w-full gap-2 text-sm"
|
||||
onClick={handleResourcePreview}
|
||||
disabled={!resourceText.trim() || resourceEnriching}
|
||||
>
|
||||
{resourceEnriching
|
||||
? <><Loader2 className="h-4 w-4 animate-spin" /> IA en cours…</>
|
||||
: resourceMode === 'replace'
|
||||
? <><Eye className="h-4 w-4" /> Aperçu</>
|
||||
: <><Sparkles className="h-4 w-4" /> Générer l'aperçu</>
|
||||
}
|
||||
</Button>
|
||||
|
||||
{/* Hint */}
|
||||
{!noteContent && resourceMode !== 'replace' && (
|
||||
<p className="text-[10px] text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-lg px-3 py-2">
|
||||
💡 La note est vide — le contenu de la ressource sera intégré directement.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -552,11 +552,11 @@ export function NoteInlineEditor({
|
||||
<Button variant="ghost" size="sm"
|
||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-colors', aiOpen && 'bg-primary/10 text-primary')}
|
||||
onClick={() => setAiOpen(!aiOpen)}
|
||||
title={t('ai.aiCopilot')}>
|
||||
title="IA Note">
|
||||
{isProcessingAI
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <Sparkles className="h-3.5 w-3.5" />}
|
||||
<span className="hidden sm:inline">{t('ai.aiCopilot')}</span>
|
||||
<span className="hidden sm:inline">IA Note</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user