fix: brainstorm infinite loop, ghost cursor, embedding ::vector cast, semantic search, billing stats, usage meter accordion
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
- Fix useBrainstormSocket: stable guestId via useRef, remove setState in cleanup - Fix GhostCursor: direct DOM manipulation via refs, no useState re-renders - Fix all SQL embedding queries: add ::vector cast on text columns - Fix embedding truncation to 15000 chars (under 8192 token limit) - Fix NoteEmbedding INSERT: remove non-existent updatedAt column - Fix billing page: show all quota stats in grid instead of single metric - Fix usage meter: accordion expand/collapse, per-feature detail - Fix semantic search: rebuild 103 note embeddings, ::vector cast on vectorSearch - Fix brainstorm expand/manual-idea/create: ::vector cast on embedding SQL
This commit is contained in:
@@ -6,9 +6,8 @@ import { DefaultChatTransport } from 'ai'
|
||||
import type { UIMessage } from 'ai'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
X, Bot, Sparkles, History, Send, Globe, Briefcase, Palette, GraduationCap, Coffee,
|
||||
Loader2, Layers, Square, Plus, ChevronRight, MessageSquare, FileCode,
|
||||
Zap, Network, Clock, Scissors, Languages, Layout, ArrowRightLeft, BookOpen,
|
||||
X, Bot, Sparkles, History, Send, Globe,
|
||||
Loader2, Square, Plus, MessageSquare, FileCode,
|
||||
Maximize2, Minimize2
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -28,14 +27,7 @@ function getTextContent(msg: UIMessage): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
const TONE_IDS = [
|
||||
{ id: 'professional', icon: Briefcase },
|
||||
{ id: 'creative', icon: Palette },
|
||||
{ id: 'academic', icon: GraduationCap },
|
||||
{ id: 'casual', icon: Coffee },
|
||||
] as const
|
||||
|
||||
type AITab = 'discussion' | 'actions' | 'explore' | 'resources'
|
||||
type AITab = 'discussion' | 'history'
|
||||
|
||||
export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: boolean } = {}) {
|
||||
const { t, language } = useLanguage()
|
||||
@@ -45,7 +37,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [aiTab, setAiTab] = useState<AITab>('discussion')
|
||||
const [selectedTone, setSelectedTone] = useState('professional')
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [chatScope, setChatScope] = useState<'all' | string>('all')
|
||||
const [input, setInput] = useState('')
|
||||
@@ -55,9 +46,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
const [history, setHistory] = useState<any[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
|
||||
const [insights, setInsights] = useState<string>('')
|
||||
const [insightsLoading, setInsightsLoading] = useState(false)
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current
|
||||
|
||||
@@ -65,6 +53,23 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
transport,
|
||||
onError: (error) => {
|
||||
console.error('Chat error:', error)
|
||||
try {
|
||||
const parsed = JSON.parse((error as Error).message || '{}')
|
||||
if (parsed.error === 'QUOTA_EXCEEDED') {
|
||||
const isBasic = (parsed.currentTier || 'BASIC') === 'BASIC'
|
||||
toast.error(
|
||||
language === 'fr'
|
||||
? isBasic
|
||||
? 'Le chat IA est réservé au plan PRO et supérieur.'
|
||||
: `Limite mensuelle atteinte pour le plan ${parsed.currentTier}. Elle se réinitialise le mois prochain.`
|
||||
: isBasic
|
||||
? 'AI Chat is available from the PRO plan onwards.'
|
||||
: `Monthly quota reached for ${parsed.currentTier} plan. It will reset next month.`,
|
||||
{ duration: 8000 }
|
||||
)
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
toast.error(t('chat.assistantError') || 'Chat error')
|
||||
}
|
||||
})
|
||||
@@ -91,7 +96,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
{ text },
|
||||
{
|
||||
body: {
|
||||
tone: selectedTone,
|
||||
chatScope,
|
||||
notebookId: chatScope !== 'all' ? chatScope : undefined,
|
||||
webSearch: webSearch && webSearchAvailable,
|
||||
@@ -115,24 +119,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
|
||||
const fetchInsights = async () => {
|
||||
setInsightsLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/chat/insights')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setInsights(data.insight)
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
setInsightsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (aiTab === 'discussion') {
|
||||
// history is loaded on demand via insights tab
|
||||
}
|
||||
}, [aiTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
@@ -203,6 +189,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
|
||||
title={t('general.close') || 'Fermer'}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -215,22 +202,28 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border px-2 shrink-0">
|
||||
{(['discussion', 'actions', 'explore', 'resources'] as AITab[]).map((tab) => {
|
||||
{(['discussion', 'history'] as AITab[]).map((tab) => {
|
||||
const labels: Record<AITab, string> = {
|
||||
discussion: t('ai.chatTab') || 'Discussion',
|
||||
actions: t('ai.actionsTab') || 'Actions',
|
||||
explore: t('ai.exploreTab') || 'Explorer',
|
||||
resources: t('ai.resourcesTab') || 'Ressources',
|
||||
history: t('ai.historyTab') || 'Historique',
|
||||
}
|
||||
const icons: Record<AITab, React.ReactNode> = {
|
||||
discussion: <MessageSquare size={12} />,
|
||||
history: <History size={12} />,
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setAiTab(tab)}
|
||||
onClick={() => {
|
||||
setAiTab(tab)
|
||||
if (tab === 'history') fetchHistory()
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 py-3 text-[10px] uppercase tracking-[0.2em] font-bold transition-all relative",
|
||||
"flex-1 py-3 text-[10px] uppercase tracking-[0.2em] font-bold transition-all relative flex items-center justify-center gap-1.5",
|
||||
aiTab === tab ? 'text-brand-accent' : 'text-concrete hover:text-ink/60'
|
||||
)}
|
||||
>
|
||||
{icons[tab]}
|
||||
{labels[tab]}
|
||||
{aiTab === tab && (
|
||||
<motion.div
|
||||
@@ -248,38 +241,15 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
<AnimatePresence mode="wait">
|
||||
{aiTab === 'discussion' && (
|
||||
<motion.div key="discussion" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
|
||||
{/* Context selector */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.writingTone')}</label>
|
||||
<div className="flex items-center gap-1">
|
||||
{TONE_IDS.map((tone) => {
|
||||
const Icon = tone.icon
|
||||
return (
|
||||
<button
|
||||
key={tone.id}
|
||||
onClick={() => setSelectedTone(tone.id)}
|
||||
title={t(`ai.tones.${tone.id}`)}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg flex items-center justify-center text-[9px] font-bold transition-all border',
|
||||
selectedTone === tone.id
|
||||
? 'bg-brand-accent text-white border-brand-accent shadow-sm'
|
||||
: 'bg-white/50 dark:bg-white/5 border-border/40 text-concrete hover:border-brand-accent/40'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scope selector */}
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => setChatScope('all')}
|
||||
className={cn(
|
||||
'w-full p-2.5 border rounded-xl text-[11px] flex items-center justify-between transition-all',
|
||||
chatScope === 'all' ? 'bg-brand-accent/5 border-brand-accent/30' : 'bg-white/50 dark:bg-white/5 border-border/40 hover:border-ink/20'
|
||||
)}
|
||||
title={t('ai.allMyNotes') || 'Toutes mes notes'}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<FileCode size={14} className="text-brand-accent/60" />
|
||||
@@ -324,14 +294,14 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
<div key={msg.id} className={cn('flex gap-3', msg.role === 'user' && 'flex-row-reverse')}>
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded-xl flex items-center justify-center flex-shrink-0',
|
||||
msg.role === 'user' ? 'bg-ink text-paper' : 'bg-brand-accent/10 text-brand-accent',
|
||||
msg.role === 'user' ? 'bg-brand-accent text-white' : 'bg-brand-accent/10 text-brand-accent',
|
||||
)}>
|
||||
{msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'max-w-[85%] p-3.5 rounded-2xl text-sm leading-relaxed',
|
||||
msg.role === 'user'
|
||||
? 'bg-ink text-paper rounded-tr-sm'
|
||||
? 'bg-brand-accent text-white rounded-tr-sm'
|
||||
: 'bg-white/60 dark:bg-white/5 border border-border rounded-tl-sm text-ink',
|
||||
)}>
|
||||
{msg.role === 'assistant' ? <MarkdownContent content={text} /> : <p>{text}</p>}
|
||||
@@ -354,187 +324,36 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{aiTab === 'actions' && (
|
||||
<motion.div key="actions" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-8">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Transformations</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ icon: <Sparkles size={14} />, label: t('ai.actionClarify') || 'Clarifier' },
|
||||
{ icon: <Scissors size={14} />, label: t('ai.actionShorten') || 'Raccourcir' },
|
||||
{ icon: <Zap size={14} />, label: t('ai.actionImprove') || 'Améliorer' },
|
||||
{ icon: <Languages size={14} />, label: t('ai.actionTranslate') || 'Traduire' },
|
||||
].map((action, i) => (
|
||||
<button key={i} className="flex flex-col items-center gap-3 p-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl transition-all group hover:border-ink/20">
|
||||
<div className="p-2 rounded-lg bg-slate-50 dark:bg-white/10 transition-colors group-hover:bg-brand-accent group-hover:text-white shadow-sm text-concrete">
|
||||
{action.icon}
|
||||
{aiTab === 'history' && (
|
||||
<motion.div key="history" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-3">
|
||||
{historyLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-concrete" />
|
||||
</div>
|
||||
) : history.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-concrete/30">
|
||||
<History size={24} />
|
||||
<p className="text-[11px] mt-3 italic">{t('ai.noHistory') || 'Aucun historique'}</p>
|
||||
</div>
|
||||
) : (
|
||||
history.map((conv: any) => (
|
||||
<button
|
||||
key={conv.id}
|
||||
onClick={() => { setConversationId(conv.id); setMessages(conv.messages || []); setAiTab('discussion') }}
|
||||
className="w-full text-left p-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl hover:border-brand-accent/30 transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-brand-accent/10 text-brand-accent group-hover:bg-brand-accent group-hover:text-white transition-colors">
|
||||
<MessageSquare size={14} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-ink truncate">{conv.title || conv.id}</p>
|
||||
<p className="text-[10px] text-concrete mt-0.5">{new Date(conv.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-ink/80 uppercase tracking-widest">{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<button className="col-span-2 flex items-center justify-center gap-3 py-3 px-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl text-[10px] font-bold text-ink/80 hover:bg-white dark:hover:bg-white/10 transition-colors hover:border-ink/20 uppercase tracking-widest">
|
||||
<FileCode size={14} className="text-concrete" />
|
||||
{t('ai.actionMarkdown') || 'Convertir en Markdown'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Generation Tools</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
<div className="group relative p-6 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all duration-500 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Layout size={80} className="text-brand-accent" />
|
||||
</div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-50 rounded-lg text-brand-accent"><Layout size={18} /></div>
|
||||
<div className="space-y-0.5">
|
||||
<h5 className="text-sm font-bold text-ink leading-none">{t('ai.slides') || 'Présentation'}</h5>
|
||||
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.slidesDesc') || 'Convertir en slides interactives'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 uppercase tracking-[0.2em]">
|
||||
{t('ai.generate') || 'Générer'} <ArrowRightLeft size={14} className="opacity-60" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group relative p-6 rounded-2xl bg-white border border-border hover:border-emerald-500/30 transition-all duration-500 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<BookOpen size={80} className="text-emerald-500" />
|
||||
</div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-50 rounded-lg text-emerald-500"><BookOpen size={18} /></div>
|
||||
<div className="space-y-0.5">
|
||||
<h5 className="text-sm font-bold text-ink leading-none">{t('ai.diagram') || 'Diagramme'}</h5>
|
||||
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.diagramDesc') || 'Visualisation de structure'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-full py-3.5 bg-emerald-600 text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-emerald-600/20 uppercase tracking-[0.2em]">
|
||||
{t('ai.trace') || 'Tracer'} <ArrowRightLeft size={14} className="opacity-60" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2 opacity-20 py-4">
|
||||
<History size={16} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest whitespace-nowrap">Auto-Save Enabled</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{aiTab === 'explore' && (
|
||||
<motion.div key="explore" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Intelligence Modules</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all text-left overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Zap size={60} className="text-brand-accent" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-4">
|
||||
<div className="p-3 bg-brand-accent/10 rounded-xl text-brand-accent group-hover:bg-brand-accent group-hover:text-white transition-colors">
|
||||
<Zap size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-ink text-sm">{t('ai.brainstormWave') || 'Brainstorm Wave'}</h5>
|
||||
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.brainstormWaveDesc') || 'Unfold dimensions of thought'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-indigo-500/30 transition-all text-left overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Network size={60} className="text-indigo-500" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-4">
|
||||
<div className="p-3 bg-indigo-500/10 rounded-xl text-indigo-500 group-hover:bg-indigo-500 group-hover:text-white transition-colors">
|
||||
<Network size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-ink text-sm">{t('ai.semanticNetwork') || 'Réseau Sémantique'}</h5>
|
||||
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.semanticNetworkDesc') || 'Detect clusters and bridges'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-rose-500/30 transition-all text-left overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Clock size={60} className="text-rose-500" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-4">
|
||||
<div className="p-3 bg-rose-500/10 rounded-xl text-rose-500 group-hover:bg-rose-500 group-hover:text-white transition-colors">
|
||||
<Clock size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-ink text-sm">{t('ai.temporalForecast') || 'Prévision Temporelle'}</h5>
|
||||
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.temporalForecastDesc') || 'Predict relevance recurrence'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 rounded-2xl bg-slate-50 dark:bg-white/5 border border-dashed border-border">
|
||||
<p className="text-[10px] text-concrete leading-relaxed font-medium italic text-center">
|
||||
{t('ai.modulesHint') || 'Ces modules utilisent les embeddings pour analyser vos pensées.'}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{aiTab === 'resources' && (
|
||||
<motion.div key="resources" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resourceUrl') || 'URL (Optionnel)'}</label>
|
||||
<div className="relative">
|
||||
<input type="text" placeholder="https://..." className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl pl-4 pr-10 py-3 text-xs outline-none focus:border-brand-accent transition-colors text-ink" />
|
||||
<Globe size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-concrete/40" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resourceText') || 'Texte de la ressource'}</label>
|
||||
<textarea
|
||||
rows={8}
|
||||
placeholder={t('ai.resourcePlaceholder') || 'Collez votre texte ici...'}
|
||||
className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl p-4 text-xs outline-none focus:border-brand-accent transition-colors resize-none leading-relaxed text-ink"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.integrationMode') || "Mode d'intégration"}</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ id: 'replace', label: t('ai.modeReplace') || 'Remplacer', sub: 'Direct' },
|
||||
{ id: 'append', label: t('ai.modeAppend') || 'Compléter', sub: 'Ajoute' },
|
||||
{ id: 'merge', label: t('ai.modeMerge') || 'Fusionner', sub: 'Intègre' },
|
||||
].map((mode) => (
|
||||
<button key={mode.id} className="flex flex-col items-center justify-center p-3 rounded-xl border transition-all text-center bg-white border-border hover:bg-slate-50 dark:hover:bg-white/5">
|
||||
<span className="text-[10px] font-bold text-ink">{mode.label}</span>
|
||||
<span className="text-[8px] text-concrete opacity-60 leading-tight mt-1 font-medium">{mode.sub}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full py-4 bg-brand-accent text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-brand-accent/20">
|
||||
<Sparkles size={18} />
|
||||
{t('ai.generatePreview') || "Générer l'aperçu"}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -561,6 +380,15 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="absolute left-6 bottom-4 flex gap-3 text-concrete/40">
|
||||
<button
|
||||
onClick={() => webSearchAvailable && setWebSearch(!webSearch)}
|
||||
className={cn("hover:text-brand-accent transition-colors", webSearch && "text-brand-accent")}
|
||||
title={webSearch ? (t('ai.webSearchEnabled') || 'Recherche web activée') : (t('ai.webSearchDisabled') || 'Activer la recherche web')}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute right-4 bottom-4 flex flex-col gap-2">
|
||||
{isLoading ? (
|
||||
<button onClick={() => stop()} className="p-2.5 bg-rose-500 text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-rose-500/20">
|
||||
@@ -572,18 +400,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute left-6 bottom-4 flex gap-3 text-concrete/40">
|
||||
<button
|
||||
onClick={() => webSearchAvailable && setWebSearch(!webSearch)}
|
||||
className={cn("hover:text-brand-accent transition-colors", webSearch && "text-brand-accent")}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</button>
|
||||
<button className="hover:text-brand-accent transition-colors"><Network size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<p className="text-[9px] text-concrete/40 uppercase tracking-[0.3em] font-bold">Shift+Enter for new line</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user