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 && (
)}
{t('studio.inputHint')} · /clear
)
}