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 && (
)}
{error ? (
⚠️ Hata: {error}
) : typeof answer === 'string' ? (
renderFormattedText(answer)
) : Array.isArray(answer) ? (
answer.length === 0 ? (
Sonuç bulunamadı.
) : (
{Object.keys(answer[0]).map((col) => (
|
{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 (
|
{display}
|
)
})}
))}
)
) : typeof answer === 'object' && answer !== null && (answer as any).message ? (
renderFormattedText(String((answer as any).message))
) : (
{JSON.stringify(answer, null, 2)}
)}
{chart && (
)}
)
}
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"
>
} onClick={handleNewConversation} disabled={loading}>
{translate('::App.Platform.NewChat')}
{conversations.map((conversation) => (
))}
}
onClick={handleNewConversation}
disabled={loading}
>
{translate('::App.Platform.NewChat')}
}
onClick={() => setIsConversationDrawerOpen(true)}
>
{translate('::App.Platform.Chats')}
(
)}
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')}
)}
)
}
export default Assistant