import { useState, useRef, useEffect, useCallback } from 'react' import { useI18n } from '../i18n' function renderContent(text) { const parts = [] const codeBlockRegex = /(```[\s\S]*?```)/g let match let lastIndex = 0 while ((match = codeBlockRegex.exec(text)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', content: text.slice(lastIndex, match.index) }) } const full = match[1] const firstNewline = full.indexOf('\n') const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : '' const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3) parts.push({ type: 'code', lang, content: code }) lastIndex = match.index + full.length } if (lastIndex < text.length) { parts.push({ type: 'text', content: text.slice(lastIndex) }) } return parts } function formatText(text) { return text .replace(/&/g, '&').replace(//g, '>') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^\s*[-*] (.+)$/gm, '$1') .replace(/^\s*(\d+)[.)] (.+)$/gm, '$1$2') } function FeedItem({ msg }) { const isUser = msg.role === 'user' const isSystem = msg.role === 'system' const roleLabel = isUser ? null : isSystem ? null : (
) const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' if (isSystem) { return (
{msg.content}
{timeStr && {timeStr}}
) } return (
{roleLabel}
{isUser ? 'Vous' : 'IA'} {timeStr && {timeStr}}
{renderContent(msg.content).map((part, i) => part.type === 'code' ? (
{part.lang &&
{part.lang}
}
{part.content}
) : ( ) )}
) } function StreamingItem({ content }) { return (
IA
{renderContent(content).map((part, i) => part.type === 'code' ? (
{part.lang &&
{part.lang}
}
{part.content}
) : ( ) )}
) } export default function Studio({ api }) { const { t } = useI18n() const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [streaming, setStreaming] = useState('') const [loaded, setLoaded] = useState(false) const messagesEnd = useRef(null) const textareaRef = useRef(null) useEffect(() => { api.getChatHistory().then(data => { if (data.messages && data.messages.length > 0) { setMessages(data.messages) } else { setMessages([ { id: 'welcome', role: 'system', content: t('studio.welcomeNew'), time: new Date().toISOString() }, ]) } setLoaded(true) }).catch(() => { setMessages([ { id: 'welcome', role: 'system', content: t('studio.welcomeNew'), time: new Date().toISOString() }, ]) setLoaded(true) }) }, []) useEffect(() => { messagesEnd.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, streaming]) useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto' textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px' } }, [input]) const handleClear = useCallback(async () => { try { await api.clearChat() setMessages([ { id: 'clear-' + Date.now(), role: 'system', content: t('studio.cleared'), time: new Date().toISOString() }, ]) } catch {} }, [api, t]) const handleSend = useCallback(async () => { if (!input.trim() || loading) return const text = input.trim() setInput('') if (text === '/clear') { handleClear() return } const userMsg = { id: Date.now().toString(), role: 'user', content: text, time: new Date().toISOString() } setMessages(prev => [...prev, userMsg]) setLoading(true) setStreaming('') try { let accumulated = '' await api.sendChat(text, true, (partial) => { accumulated = partial setStreaming(partial) }) const finalContent = accumulated || t('studio.noResponse') setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', content: finalContent, time: new Date().toISOString(), }]) } catch (err) { setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `${t('studio.error')}: ${err.message}`, time: new Date().toISOString(), }]) } finally { setLoading(false) setStreaming('') } }, [input, loading, api, t, handleClear]) const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() } } if (!loaded) { return (
) } return (
{messages.map(msg => ( ))} {streaming && } {loading && !streaming && (
)}