'use client' import { useState, useRef, useEffect, useCallback } from 'react' import { useChat } from '@ai-sdk/react' import { DefaultChatTransport } from 'ai' import type { UIMessage } from 'ai' import { cn } from '@/lib/utils' import { X, Bot, Sparkles, History, Send, Globe, Loader2, Square, Plus, MessageSquare, FileCode, Maximize2, Minimize2 } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { MarkdownContent } from '@/components/markdown-content' import { useWebSearchAvailable } from '@/hooks/use-web-search-available' import { useNotebooks } from '@/context/notebooks-context' import { HierarchicalNotebookSelector } from '@/components/hierarchical-notebook-selector' import { toast } from 'sonner' import { createConversation } from '@/app/actions/chat-actions' import { motion, AnimatePresence } from 'motion/react' function getTextContent(msg: UIMessage): string { if (msg.parts && Array.isArray(msg.parts)) { return msg.parts.filter((p: any) => p.type === 'text' && typeof p.text === 'string').map((p: any) => p.text).join('') } if (typeof (msg as any).content === 'string') return (msg as any).content return '' } type AITab = 'discussion' | 'history' export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: boolean } = {}) { const { t, language } = useLanguage() const webSearchAvailable = useWebSearchAvailable() const { notebooks } = useNotebooks() const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [aiTab, setAiTab] = useState('discussion') const [webSearch, setWebSearch] = useState(false) const [chatScope, setChatScope] = useState<'all' | string>('all') const [input, setInput] = useState('') const [conversationId, setConversationId] = useState() const [isContextualAIVisible, setIsContextualAIVisible] = useState(false) const [history, setHistory] = useState([]) const [historyLoading, setHistoryLoading] = useState(false) const messagesEndRef = useRef(null) const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current const { messages, setMessages, sendMessage, status, stop } = useChat({ 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( isBasic ? t('chat.quotaExceededBasic') : t('chat.quotaExceededTier', { tier: parsed.currentTier }), { duration: 8000 } ) return } } catch {} toast.error(t('chat.assistantError') || 'Chat error') } }) const isLoading = status === 'submitted' || status === 'streaming' const handleSend = async () => { const text = input.trim() if (!text || isLoading) return setInput('') let convId = conversationId if (!convId) { try { const result = await createConversation(text, chatScope !== 'all' ? chatScope : undefined) convId = result.id setConversationId(convId) } catch { toast.error(t('chat.createError')) return } } try { await sendMessage( { text }, { body: { 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 () => { setHistoryLoading(true) try { const res = await fetch('/api/chat/history') if (res.ok) setHistory(await res.json()) } catch (e) { console.error(e) } setHistoryLoading(false) } useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) } }, [messages]) const handleToggle = useCallback(() => setIsOpen(v => !v), []) const handleVisibility = useCallback((e: any) => setIsContextualAIVisible(e.detail), []) useEffect(() => { window.addEventListener('toggle-ai-chat', handleToggle) window.addEventListener('contextual-ai-visibility', handleVisibility) return () => { window.removeEventListener('toggle-ai-chat', handleToggle) window.removeEventListener('contextual-ai-visibility', handleVisibility) } }, [handleToggle, handleVisibility]) // Floating trigger when closed if (!isOpen) { if (!showFloatingTrigger || isContextualAIVisible) return null return ( setIsOpen(true)} className="fixed bottom-6 end-6 h-12 w-12 rounded-2xl shadow-xl z-40 bg-brand-accent text-white hover:scale-105 border border-brand-accent/20" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} title={t('ai.openAssistant')} > ) } return (
{/* Header */}

{t('ai.assistantTitle')}

{t('ai.poweredByMomento')}

{/* Tabs */}
{(['discussion', 'history'] as AITab[]).map((tab) => { const labels: Record = { discussion: t('ai.chatTab') || 'Discussion', history: t('ai.historyTab') || 'Historique', } const icons: Record = { discussion: , history: , } return ( ) })}
{/* Content */}
{aiTab === 'discussion' && ( {/* Scope selector */}
+ Carnet
!nb.trashedAt)} selectedId={chatScope !== 'all' ? chatScope : null} onSelect={(id) => setChatScope(id)} placeholder={t('ai.selectNotebook') || 'Inclure un carnet...'} dropUp />
{/* Messages */} {messages.length === 0 && (

{t('ai.welcomeMsg')}

)} {messages.map((msg: UIMessage) => { const text = getTextContent(msg) if (msg.role === 'assistant' && !text) return null return (
{msg.role === 'user' ? 'U' : }
{msg.role === 'assistant' ? :

{text}

}
) })} {isLoading && (
)}
)} {aiTab === 'history' && ( {historyLoading ? (
) : history.length === 0 ? (

{t('ai.noHistory') || 'Aucun historique'}

) : ( history.map((conv: any) => ( )) )}
)}
{/* Textarea — only on discussion tab */} {aiTab === 'discussion' && (