fix: add missing error handling for sendMessage promise rejections

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-09 14:15:38 +00:00
parent b0c2556a12
commit 66e957fd59
3 changed files with 69 additions and 51 deletions

View File

@@ -83,19 +83,24 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
}
}
await sendMessage(
{ text },
{
body: {
tone: selectedTone,
chatScope,
notebookId: chatScope !== 'all' ? chatScope : undefined,
webSearch: webSearch && webSearchAvailable,
conversationId: convId,
language,
try {
await sendMessage(
{ text },
{
body: {
tone: selectedTone,
chatScope,
notebookId: chatScope !== 'all' ? chatScope : undefined,
webSearch: webSearch && webSearchAvailable,
conversationId: convId,
language,
}
}
}
)
)
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
}
const fetchHistory = async () => {

View File

@@ -137,17 +137,22 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila
}
}
await sendMessage(
{ text: content },
{
body: {
conversationId: convId,
notebookId: notebookId || selectedNotebook || undefined,
language,
webSearch: webSearchEnabled,
},
}
)
try {
await sendMessage(
{ text: content },
{
body: {
conversationId: convId,
notebookId: notebookId || selectedNotebook || undefined,
language,
webSearch: webSearchEnabled,
},
}
)
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
}
const handleNewChat = () => {

View File

@@ -220,7 +220,12 @@ export function ContextualAIChat({
const text = input.trim()
if (!text || isLoading) return
setInput('')
await sendMessage({ text }, { body: buildChatBody() })
try {
await sendMessage({ text }, { body: buildChatBody() })
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
}
// ── Action execution ────────────────────────────────────────────────────────
@@ -493,7 +498,7 @@ export function ContextualAIChat({
<div className="flex items-start justify-between">
<div className="min-w-0 space-y-2">
<h2 className="font-serif text-xl font-medium text-[#1C1C1C] flex items-center gap-2 leading-tight">
<Sparkles className="h-[18px] w-[18px] shrink-0 text-[#D4A373]" />
<Sparkles className="h-[18px] w-[18px] shrink-0 text-memento-accent" />
IA Assistant
</h2>
<p className="text-[11px] text-[#1C1C1C]/60 uppercase tracking-wider font-medium opacity-60 truncate">
@@ -510,9 +515,12 @@ export function ContextualAIChat({
</button>
<button
onClick={onClose}
className="p-1.5 hover:bg-white/40 rounded-full transition-colors text-[#1C1C1C]/40"
className="p-1.5 hover:bg-white/40 rounded-full transition-colors text-[#1C1C1C]/40 group"
>
<ChevronRight size={20} />
<div className="relative w-5 h-5 flex items-center justify-center">
<ChevronRight size={20} className="transition-all duration-200 group-hover:opacity-0 group-hover:scale-0" />
<X size={18} className="absolute inset-0 m-auto opacity-0 scale-0 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
</div>
</button>
</div>
</div>
@@ -534,7 +542,7 @@ export function ContextualAIChat({
>
{tab.label}
{activeTab === tab.id && (
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#75B2D6]" />
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-memento-accent" />
)}
</button>
))}
@@ -562,7 +570,7 @@ export function ContextualAIChat({
{resourcePreview && (
<div className="absolute inset-0 z-20 flex flex-col bg-[#FDFDFE]/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300">
<div className="px-6 py-4 border-b border-border/40 flex items-center justify-between shrink-0">
<p className="text-[10px] font-bold uppercase tracking-widest text-[#75B2D6]">
<p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
{resourcePreview.source === 'chat' ? 'Injecter depuis Discussion' : 'Aperçu IA'}
</p>
<button onClick={() => setResourcePreview(null)} className="text-[#1C1C1C]/40 hover:text-[#1C1C1C]">
@@ -576,7 +584,7 @@ export function ContextualAIChat({
</div>
<div className="p-6 border-t border-border flex gap-3 shrink-0">
<button onClick={() => setResourcePreview(null)} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#1C1C1C]/40 hover:text-[#1C1C1C] transition-all">ANNULER</button>
<button onClick={handleApplyResourcePreview} className="flex-1 py-3.5 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg shadow-[#75B2D6]/20 transition-all hover:opacity-90">APPLIQUER À LA NOTE</button>
<button onClick={handleApplyResourcePreview} className="flex-1 py-3.5 bg-memento-blue text-white rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg shadow-memento-blue/20 transition-all hover:opacity-90">APPLIQUER À LA NOTE</button>
</div>
</div>
)}
@@ -594,7 +602,7 @@ export function ContextualAIChat({
{messages.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-center space-y-6 py-12">
<div className="w-20 h-20 rounded-full bg-white/40 backdrop-blur-sm border border-dashed border-border flex items-center justify-center shadow-sm">
<MessageSquare size={32} className="text-[#75B2D6]/60" />
<MessageSquare size={32} className="text-memento-blue/60" />
</div>
<p className="text-xs font-serif italic text-[#1C1C1C]/40 leading-relaxed max-w-[200px]">Posez une question à l'Assistant pour commencer.</p>
</div>
@@ -623,7 +631,7 @@ export function ContextualAIChat({
{isLoading && (
<div className="flex flex-col gap-3">
<div className="bg-white/60 backdrop-blur-sm border border-border p-5 rounded-2xl shadow-sm w-fit">
<div className="flex gap-1.5"><span className="w-1.5 h-1.5 bg-[#75B2D6] rounded-full animate-pulse" /><span className="w-1.5 h-1.5 bg-[#75B2D6] rounded-full animate-pulse delay-75" /><span className="w-1.5 h-1.5 bg-[#75B2D6] rounded-full animate-pulse delay-150" /></div>
<div className="flex gap-1.5"><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse" /><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse delay-75" /><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse delay-150" /></div>
</div>
</div>
)}
@@ -664,11 +672,11 @@ export function ContextualAIChat({
className={cn(
'h-[52px] rounded-xl border transition-all flex flex-col items-center justify-center gap-1.5 shadow-sm',
isActive
? 'bg-[#75B2D6]/10 border-[#75B2D6] text-[#75B2D6]'
? 'bg-memento-blue/10 border-memento-blue text-memento-blue'
: 'bg-white/60 border-border text-[#1C1C1C]/40 hover:border-[#1C1C1C]/20'
)}
>
<Icon size={14} className={isActive ? 'text-[#75B2D6]' : 'text-[#1C1C1C]/40'} />
<Icon size={14} className={isActive ? 'text-memento-blue' : 'text-[#1C1C1C]/40'} />
<span className="text-[9px] font-bold uppercase tracking-tight">{tone.label}</span>
</button>
)
@@ -680,7 +688,7 @@ export function ContextualAIChat({
<div className="relative">
<textarea
rows={4}
className="w-full bg-white/60 border border-border rounded-2xl p-5 pr-14 text-sm outline-none focus:border-[#75B2D6] transition-all resize-none leading-relaxed font-light custom-scrollbar shadow-sm text-[#1C1C1C]"
className="w-full bg-white/60 border border-border rounded-2xl p-5 pr-14 text-sm outline-none focus:border-memento-blue transition-all resize-none leading-relaxed font-light custom-scrollbar shadow-sm text-[#1C1C1C]"
placeholder="Posez votre question sur cette note..."
value={input}
onChange={e => setInput(e.target.value)}
@@ -690,12 +698,12 @@ export function ContextualAIChat({
<div className="absolute right-4 bottom-4 flex gap-2">
<button
onClick={() => setWebSearch(!webSearch)}
className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-[#75B2D6] bg-[#75B2D6]/10" : "text-[#1C1C1C]/20 hover:text-[#1C1C1C]")}
className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-memento-blue bg-memento-blue/10" : "text-[#1C1C1C]/20 hover:text-[#1C1C1C]")}
title="Web Search"
>
<Globe size={18} />
</button>
<button onClick={handleSend} disabled={!input.trim() || isLoading} className="p-2.5 bg-[#75B2D6] text-white rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-[#75B2D6]/20 disabled:opacity-30">
<button onClick={handleSend} disabled={!input.trim() || isLoading} className="p-2.5 bg-memento-blue text-white rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-memento-blue/20 disabled:opacity-30">
<Send size={18} />
</button>
</div>
@@ -725,7 +733,7 @@ export function ContextualAIChat({
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
onClick={onUndoLastAction}
className="w-full py-3.5 bg-[#75B2D6]/20 border border-[#75B2D6]/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-[#75B2D6] uppercase tracking-[0.2em] hover:bg-[#75B2D6]/30 transition-all shadow-md"
className="w-full py-3.5 bg-memento-blue/20 border border-memento-blue/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-memento-blue uppercase tracking-[0.2em] hover:bg-memento-blue/30 transition-all shadow-md"
>
<RotateCcw size={12} /> {t('ai.undoLastAction')}
</motion.button>
@@ -736,11 +744,11 @@ export function ContextualAIChat({
const isActive = action.id === 'translate' && showLangPicker
const Icon = action.icon
return (
<button key={i} onClick={() => action.id === 'translate' ? setShowLangPicker(v => !v) : handleAction(action)} disabled={!!actionLoading} className={cn("flex flex-col items-center gap-3 p-4 bg-white/40 backdrop-blur-sm border rounded-xl transition-all group shadow-sm", isActive ? "border-[#75B2D6] bg-[#75B2D6]/5" : "border-border hover:border-[#1C1C1C]/20")}>
<div className={cn("p-2 rounded-lg bg-white/60 transition-colors group-hover:bg-[#1C1C1C] group-hover:text-[#FDFDFE] shadow-sm", loading && "animate-pulse", isActive && "bg-[#75B2D6] text-white")}>
<button key={i} onClick={() => action.id === 'translate' ? setShowLangPicker(v => !v) : handleAction(action)} disabled={!!actionLoading} className={cn("flex flex-col items-center gap-3 p-4 bg-white/40 backdrop-blur-sm border rounded-xl transition-all group shadow-sm", isActive ? "border-memento-blue bg-memento-blue/5" : "border-border hover:border-[#1C1C1C]/20")}>
<div className={cn("p-2 rounded-lg bg-white/60 transition-colors group-hover:bg-[#1C1C1C] group-hover:text-[#FDFDFE] shadow-sm", loading && "animate-pulse", isActive && "bg-memento-blue text-white")}>
{loading ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
</div>
<span className={cn("text-[10px] font-bold uppercase tracking-widest", isActive ? "text-[#75B2D6]" : "text-[#1C1C1C]/80")}>{t(action.i18nKey)}</span>
<span className={cn("text-[10px] font-bold uppercase tracking-widest", isActive ? "text-memento-blue" : "text-[#1C1C1C]/80")}>{t(action.i18nKey)}</span>
</button>
)
})}
@@ -763,7 +771,7 @@ export function ContextualAIChat({
className={cn(
"py-2 px-1 rounded-lg border text-[10px] font-bold uppercase tracking-tighter transition-all",
translateTarget === lang
? "bg-[#75B2D6] border-[#75B2D6] text-white shadow-md shadow-[#75B2D6]/20"
? "bg-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/20"
: "bg-white/60 border-border text-[#1C1C1C]/60 hover:border-[#1C1C1C]/20"
)}
>
@@ -789,7 +797,7 @@ export function ContextualAIChat({
<button
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'translate')!, translateTarget)}
disabled={!translateTarget || !!actionLoading}
className="w-full py-3 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-[#75B2D6]/20"
className="w-full py-3 bg-memento-blue text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-memento-blue/20"
>
<Languages size={14} /> {t('ai.translateNow')}
</button>
@@ -827,13 +835,13 @@ export function ContextualAIChat({
<div className="h-px flex-1 bg-border/40" />
</div>
<div className="group relative p-6 rounded-2xl bg-white/40 backdrop-blur-sm border border-border hover:border-[#75B2D6]/30 transition-all duration-500 overflow-hidden shadow-sm">
<div className="group relative p-6 rounded-2xl bg-white/40 backdrop-blur-sm border border-border hover:border-memento-blue/30 transition-all duration-500 overflow-hidden shadow-sm">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Layout size={80} className="text-[#75B2D6]" />
<Layout size={80} className="text-memento-blue" />
</div>
<div className="relative space-y-5">
<div className="flex items-center gap-3">
<div className="p-2 bg-white/60 rounded-lg text-[#75B2D6]"><Layout size={18} /></div>
<div className="p-2 bg-white/60 rounded-lg text-memento-blue"><Layout size={18} /></div>
<div className="space-y-0.5">
<h5 className="text-sm font-bold text-[#1C1C1C] leading-none">{t('ai.generate.slides')}</h5>
<p className="text-[9px] text-[#1C1C1C]/40 uppercase tracking-tight">{t('ai.generate.sectionLabel')}</p>
@@ -850,14 +858,14 @@ export function ContextualAIChat({
</div>
<div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.generate.style')}</span>
<select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-[#75B2D6]/10 transition-all cursor-pointer text-[#1C1C1C]">
<select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-memento-blue/10 transition-all cursor-pointer text-[#1C1C1C]">
<option value="professional">{t('ai.generate.styleProfessional')}</option>
<option value="creative">Creative</option>
<option value="brutalist">Brutalist</option>
</select>
</div>
</div>
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-[#75B2D6]/20 uppercase tracking-[0.2em] disabled:opacity-50">
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-memento-blue text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-memento-blue/20 uppercase tracking-[0.2em] disabled:opacity-50">
{generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>}
</button>
@@ -865,10 +873,10 @@ export function ContextualAIChat({
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-[#75B2D6]/10 border border-[#75B2D6]/20 rounded-xl space-y-3"
className="mt-4 p-4 bg-memento-blue/10 border border-memento-blue/20 rounded-xl space-y-3"
>
<div className="flex items-center justify-between">
<span className="text-[9px] font-bold text-[#75B2D6] uppercase tracking-widest flex items-center gap-1.5">
<span className="text-[9px] font-bold text-memento-blue uppercase tracking-widest flex items-center gap-1.5">
<Check size={12} /> Présentation prête
</span>
<a
@@ -1034,7 +1042,7 @@ export function ContextualAIChat({
))}
</div>
</div>
<button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-[#75B2D6] 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-[#75B2D6]/20 disabled:opacity-50">
<button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-memento-blue 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-memento-blue/20 disabled:opacity-50">
{resourceEnriching || resourceScraping ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
{t('ai.resource.generatePreview')}
</button>