/
kreyndelHam
/
Konstructorium
Обзор
Документация
Войти
/
kreyndelHam
/
Konstructorium
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
dev
frontend/src/components/chat/SupportChatPanel.jsx
187 строк
7 KB
MihahamYT
fix(frontend): theme-safe secondary buttons and badges in dark mode
25 май 2026, 18:01
25 май 2026, 18:01
0c769bc
Код
Авторство
О чём код?
import { useState, useEffect, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from 'react-query' import { chatService } from '../../services/api' import { toast } from 'react-toastify' import { useTranslation } from '../../hooks/useTranslation' import { getSupportChatBubbleClass } from '../../utils/chatMessageClasses' /** * Parse paginated or plain list API responses. * * @param {unknown} payload * @returns {Array} */ function extractList(payload) { if (!payload) { return [] } if (Array.isArray(payload)) { return payload } if (Array.isArray(payload.results)) { return payload.results } if (Array.isArray(payload.data)) { return payload.data } return [] } /** * Admin support chat panel (human operators). * * @returns {JSX.Element} */ function SupportChatPanel() { const { t } = useTranslation() const queryClient = useQueryClient() const [message, setMessage] = useState('') const messagesEndRef = useRef(null) const { data: conversation, isLoading: conversationLoading, error: conversationError, refetch: refetchConversation } = useQuery( 'my-conversation', () => chatService.getMyConversation().then((response) => response.data), { retry: 2, retryDelay: 1000 } ) const { data: messagesResponse, isLoading: messagesLoading, refetch: refetchMessages } = useQuery( ['messages', conversation?.id], () => chatService.getMessages({ conversation: conversation?.id }).then((response) => response.data), { enabled: !!conversation?.id } ) const messages = extractList(messagesResponse) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) useEffect(() => { if (conversation?.id && messages.length > 0) { chatService.markConversationAsRead(conversation.id).catch(() => {}) } }, [conversation?.id, messages.length]) const sendMessageMutation = useMutation( ({ conversationId, content }) => chatService.createMessage({ conversation: conversationId, content }), { onSuccess: () => { setMessage('') queryClient.invalidateQueries(['messages', conversation?.id]) queryClient.invalidateQueries('my-conversation') }, onError: (error) => { const errorMessage = error.response?.data?.error || error.response?.data?.detail || error.message || t('chatPage.failedSendMessage') toast.error(errorMessage) }, } ) const handleSubmit = async (event) => { event.preventDefault() const trimmedMessage = message.trim() if (!trimmedMessage) { return } if (!conversation?.id) { try { const response = await chatService.getMyConversation() const conv = response.data if (conv?.id) { sendMessageMutation.mutate({ conversationId: conv.id, content: trimmedMessage }) } } catch (error) { console.error('Error loading conversation:', error) } return } sendMessageMutation.mutate({ conversationId: conversation.id, content: trimmedMessage }) } if (conversationLoading) { return <div className="text-center py-12">{t('chatPage.loadingChat')}</div> } if (conversationError) { return ( <div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center"> <p className="text-red-600"> {conversationError.response?.data?.error || conversationError.message || t('chatPage.failedLoadConversation')} </p> </div> ) } const canSendMessage = message.trim().length > 0 return ( <div className="bg-surface rounded-lg shadow-lg flex flex-col" style={{ height: '600px' }}> <div className="border-b p-4 flex justify-between items-center"> <h2 className="font-semibold text-lg">{t('chatPage.messages')}</h2> <button type="button" onClick={() => (conversation?.id ? refetchMessages() : refetchConversation())} disabled={messagesLoading || conversationLoading} className="ui-btn-secondary disabled:opacity-50" > {t('chatPage.refreshConversations')} </button> </div> <div className="flex-1 overflow-y-auto p-4 space-y-4"> {messagesLoading && messages.length === 0 ? ( <div className="text-center text-text-muted py-8">{t('chatPage.loadingMessages')}</div> ) : messages.length === 0 ? ( <div className="text-center text-text-muted py-8">{t('chatPage.noMessages')}</div> ) : ( messages.map((msg) => { const isOwn = msg.is_own_message const isTranscript = msg.is_escalation_transcript return ( <div key={msg.id} className={`flex ${isTranscript ? 'justify-center' : isOwn ? 'justify-end' : 'justify-start'}`}> <div className={`px-4 py-2 rounded-lg ${ isTranscript ? `max-w-full lg:max-w-3xl ${getSupportChatBubbleClass({ isTranscript: true })}` : `max-w-xs lg:max-w-md ${getSupportChatBubbleClass({ isOwn })}` }`} > <div className="text-sm font-semibold mb-1"> {isTranscript ? t('aiChat.escalationTranscriptLabel') : isOwn ? t('chatPage.you') : (msg.is_from_admin ? t('chatPage.admin') : t('aiChat.assistantLabel'))} </div> <div className="text-sm whitespace-pre-wrap">{msg.content}</div> <div className="text-xs opacity-75 mt-1">{new Date(msg.created_at).toLocaleString()}</div> </div> </div> ) }) )} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="border-t p-4"> <div className="flex gap-2"> <input type="text" value={message} onChange={(event) => setMessage(event.target.value)} placeholder={conversation?.id ? t('chatPage.typeMessage') : t('chatPage.waitingConversation')} className="flex-1 px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-surface-muted" disabled={sendMessageMutation.isLoading || !conversation?.id} /> <button type="submit" disabled={!canSendMessage || sendMessageMutation.isLoading} className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50" > {sendMessageMutation.isLoading ? t('chatPage.sending') : t('chatPage.send')} </button> </div> </form> </div> ) } export default SupportChatPanel