import React, { useState, useRef, useEffect } from 'react' import { FaComment, FaPaperPlane, FaPlus, FaRobot, FaTrash } from 'react-icons/fa' import { useStoreState } from '@/store' import { Avatar, Button, Drawer, Dropdown, ScrollBar } from '@/components/ui' import { useLocalization } from '@/utils/hooks/useLocalization' import { aiService } from '@/services/ai.service' import { AiDto } from '@/proxy/ai/models' import { Container } from '@/components/shared' import { Helmet } from 'react-helmet' import { APP_NAME } from '@/constants/app.constant' import dayjs from 'dayjs' import { AI_ASSISTANT } from '@/constants/permission.constant' // Types type ChatType = 'chat' | 'query' | 'analyze' interface BaseContent { type: ChatType question: string sql: string | null answer: string | any[] chart?: string error?: string } type MessageContent = string | BaseContent interface Message { role: 'user' | 'assistant' content: MessageContent createdAt?: string } interface Conversation { id: string title: string messages: Message[] createdAt: string updatedAt: string } const CONVERSATIONS_STORAGE_KEY = 'AiConversations' const ACTIVE_CONVERSATION_STORAGE_KEY = 'AiActiveConversationId' const getConversationTitle = (message: string) => message.length > 42 ? `${message.slice(0, 42).trimEnd()}…` : message const isContentObject = (content: MessageContent): content is BaseContent => typeof content !== 'string' && 'type' in content const renderInlineText = (text: string) => text.split(/(\*\*[^*]+\*\*|"[^"\n]+")/g).map((part, index) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)} } if (part.startsWith('"') && part.endsWith('"')) { return ( {part.slice(1, -1)} ) } return {part} }) const renderFormattedText = (value: string) => { const text = value.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\r\n/g, '\n') return (
{text.split('\n').map((line, index) => { if (!line.trim()) return
const listMarker = line.match(/^[-*+]\s+/) const isListItem = Boolean(listMarker) const content = isListItem ? line.slice(listMarker![0].length) : line return isListItem ? (
{renderInlineText(content)}
) : (

{renderInlineText(content)}

) })}
) } // Main Component const Assistant = () => { // Hooks const [conversations, setConversations] = useState([]) const [activeConversationId, setActiveConversationId] = useState(null) const [isConversationsLoaded, setIsConversationsLoaded] = useState(false) const [isConversationDrawerOpen, setIsConversationDrawerOpen] = useState(false) const [, setMessages] = useState([]) const aiPosts: Message[] = [] const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [bot, setBot] = useState([]) const [selectedBot, setSelectedBot] = useState(null) const { avatar } = useStoreState((state) => state.auth.user) const inputRef = useRef(null) const bottomRef = useRef(null) const { translate } = useLocalization() const createConversation = (): Conversation => { const now = new Date().toISOString() return { id: crypto.randomUUID(), title: translate('::App.Platform.NewChat'), messages: [], createdAt: now, updatedAt: now, } } useEffect(() => { if (messages.length === 0 && aiPosts.length > 0) { setMessages(aiPosts) // artık doğrudan Message[] } }, [aiPosts]) useEffect(() => { try { const saved = localStorage.getItem(CONVERSATIONS_STORAGE_KEY) const parsed = saved ? JSON.parse(saved) : [] const savedConversations = Array.isArray(parsed) ? parsed.filter( (conversation): conversation is Conversation => conversation && typeof conversation.id === 'string' && Array.isArray(conversation.messages), ) : [] if (savedConversations.length > 0) { const savedActiveConversationId = localStorage.getItem(ACTIVE_CONVERSATION_STORAGE_KEY) setConversations(savedConversations) setActiveConversationId( savedConversations.some((conversation) => conversation.id === savedActiveConversationId) ? savedActiveConversationId : savedConversations[0].id, ) } else { const legacyPosts = JSON.parse(localStorage.getItem('AiPosts') || '[]') const conversation = createConversation() if (Array.isArray(legacyPosts) && legacyPosts.length > 0) { conversation.messages = legacyPosts const firstQuestion = legacyPosts.find((message) => message.role === 'user')?.content conversation.title = firstQuestion ? getConversationTitle(String(firstQuestion)) : conversation.title } setConversations([conversation]) setActiveConversationId(conversation.id) } } catch (error) { console.warn('AI conversations localStorage parse error', error) const conversation = createConversation() setConversations([conversation]) setActiveConversationId(conversation.id) } setIsConversationsLoaded(true) }, []) useEffect(() => { if (!isConversationsLoaded) return localStorage.setItem(CONVERSATIONS_STORAGE_KEY, JSON.stringify(conversations)) if (activeConversationId) { localStorage.setItem(ACTIVE_CONVERSATION_STORAGE_KEY, activeConversationId) } }, [activeConversationId, conversations, isConversationsLoaded]) const activeConversation = conversations.find( (conversation) => conversation.id === activeConversationId, ) const messages = activeConversation?.messages || [] const appendMessage = (conversationId: string, message: Message) => { setConversations((previous) => { const updatedConversations = previous.map((conversation) => conversation.id === conversationId ? { ...conversation, title: conversation.messages.length === 0 && message.role === 'user' ? getConversationTitle(String(message.content)) : conversation.title, messages: [...conversation.messages, message], updatedAt: message.createdAt || new Date().toISOString(), } : conversation, ) return updatedConversations.sort( (first, second) => Date.parse(second.updatedAt) - Date.parse(first.updatedAt), ) }) } const handleNewConversation = () => { if (loading) return const emptyConversation = conversations.find( (conversation) => conversation.messages.length === 0, ) if (emptyConversation) { setActiveConversationId(emptyConversation.id) setInput('') setIsConversationDrawerOpen(false) requestAnimationFrame(() => inputRef.current?.focus()) return } const conversation = createConversation() setConversations((previous) => [conversation, ...previous]) setActiveConversationId(conversation.id) setInput('') setIsConversationDrawerOpen(false) requestAnimationFrame(() => inputRef.current?.focus()) } const handleSelectConversation = (conversationId: string) => { if (loading) return setActiveConversationId(conversationId) setIsConversationDrawerOpen(false) requestAnimationFrame(() => inputRef.current?.focus()) } const handleDeleteConversation = (conversationId: string) => { if (loading) return const remainingConversations = conversations.filter( (conversation) => conversation.id !== conversationId, ) const nextConversations = remainingConversations.length ? remainingConversations : [createConversation()] setConversations(nextConversations) if (conversationId === activeConversationId) { setActiveConversationId(nextConversations[0].id) setInput('') } } // Botları çek useEffect(() => { const fetchBots = async () => { try { const result = await aiService.getList({ skipCount: 0, maxResultCount: 1000, sorting: 'name', }) if (!result?.data?.items) { console.warn('Bot listesi boş veya hatalı formatta:', result) setBot([]) return } setBot(result?.data?.items) const firstBotId = result?.data?.items?.find((item) => item.id)?.id ?? null setSelectedBot(firstBotId) } catch (error) { console.error('Bot listesi alınırken hata oluştu:', error) } } fetchBots() }, []) // Scroll to bottom useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) // Input focus after loading useEffect(() => { if (!loading) inputRef.current?.focus() }, [loading]) // Bot seçim işlemi const onBotItemClick = (eventKey: string) => setSelectedBot(eventKey) // Gönderme işlemi const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!input.trim() || !activeConversation) return const userMessage = input.trim() const conversationId = activeConversation.id const userMessageCreatedAt = new Date().toISOString() setInput('') setLoading(true) // 1️⃣ Soruyu store'a ekle appendMessage(conversationId, { role: 'user', content: userMessage, createdAt: userMessageCreatedAt, }) try { const selectedBotItem = bot.find((item) => item.id === selectedBot) const targetUrl = (selectedBotItem?.apiUrl || '').trim() if (!targetUrl) { throw new Error('AI bot URL bulunamadı.') } const response = await fetch(targetUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ biletId: conversationId, question: userMessage, sessionId: conversationId, }), }) const data = await response.json() const raw = Array.isArray(data) ? data[0] : data const mapped: BaseContent = { ...raw, result: raw.result || raw.answer || raw.error || 'Sonuç bulunamadı.', } // 2️⃣ Cevabı store'a ekle const assistantMessageCreatedAt = new Date().toISOString() appendMessage(conversationId, { role: 'assistant', content: mapped, createdAt: assistantMessageCreatedAt, }) } catch { const errorMessage = 'Üzgünüm, bir hata oluştu. Lütfen tekrar deneyin.' const assistantMessageCreatedAt = new Date().toISOString() appendMessage(conversationId, { role: 'assistant', content: errorMessage, createdAt: assistantMessageCreatedAt, }) } setLoading(false) } // Mesaj içeriği render const renderMessageContent = (message: Message) => { if (message.role === 'assistant' && isContentObject(message.content)) { const { type, sql, answer, chart, error } = message.content as BaseContent & { error?: string } const typeStyles: Record = { chat: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-200', query: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-200', analyze: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200', } const typeLabels: Record = { chat: '🗨️ ' + translate('::AI.SohbetAnswer'), query: '📊 ' + translate('::AI.DatabaseAnswer'), analyze: '📈 ' + translate('::AI.AnalizAnswer'), } const cleanedSql = (() => { try { const rawSql = decodeURIComponent(sql || '') return rawSql.replace(/^```sql\n?/, '').replace(/```$/, '') } catch { return sql } })() return (
{typeLabels[type] || type}
{sql && (
                {cleanedSql}
              
)} {error ? (
⚠️ Hata: {error}
) : typeof answer === 'string' ? ( renderFormattedText(answer) ) : Array.isArray(answer) ? ( answer.length === 0 ? (
Sonuç bulunamadı.
) : (
{Object.keys(answer[0]).map((col) => ( ))} {answer.map((row, rowIndex) => ( {Object.keys(row).map((col, colIndex) => { const val = row[col] const display = val === null || val === undefined ? '—' : typeof val === 'boolean' ? val ? 'Evet' : 'Hayır' : typeof val === 'string' && val.endsWith('T00:00:00.000Z') ? new Date(val).toLocaleDateString('tr-TR') : String(val) return ( ) })} ))}
{col}
{display}
) ) : typeof answer === 'object' && answer !== null && (answer as any).message ? ( renderFormattedText(String((answer as any).message)) ) : (
              {JSON.stringify(answer, null, 2)}
            
)} {chart && (
Grafik
)}
) } return message.role === 'assistant' ? (
{renderFormattedText(String(message.content))}
) : (
{String(message.content)}
) } // Render return ( setIsConversationDrawerOpen(false)} onRequestClose={() => setIsConversationDrawerOpen(false)} placement="left" title={translate('::App.Platform.Chats')} width={320} bodyClass="p-3" >
{conversations.map((conversation) => ( ))}
(
)} renderThumbVertical={({ style, ...props }) => (
)} >
{messages.length === 0 && (

{translate('::AI.Welcome')}

{translate('::AI.Name')}

)} {messages.map((msg, idx) => (
{msg.role === 'user' ? ( ) : ( )}
{renderMessageContent(msg)} {msg.createdAt && (
{dayjs(msg.createdAt).format('DD.MM.YYYY HH:mm')}
)}
))} {loading && (
{translate('::AI.WaitAnswer')}
)}