2026-06-20 09:09:37 +00:00
|
|
|
|
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'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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'
|
2026-03-22 15:05:08 +00:00
|
|
|
|
import dayjs from 'dayjs'
|
2026-03-28 08:23:21 +00:00
|
|
|
|
import { AI_ASSISTANT } from '@/constants/permission.constant'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-03-22 15:05:08 +00:00
|
|
|
|
createdAt?: string
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const isContentObject = (content: MessageContent): content is BaseContent =>
|
|
|
|
|
|
typeof content !== 'string' && 'type' in content
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
const renderInlineText = (text: string) =>
|
|
|
|
|
|
text.split(/(\*\*[^*]+\*\*|"[^"\n]+")/g).map((part, index) => {
|
|
|
|
|
|
if (part.startsWith('**') && part.endsWith('**')) {
|
|
|
|
|
|
return <strong key={index}>{part.slice(2, -2)}</strong>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (part.startsWith('"') && part.endsWith('"')) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<q
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="rounded-md border border-blue-200 bg-blue-50 px-1.5 py-0.5 font-medium text-blue-800 dark:border-blue-800 dark:bg-blue-950/50 dark:text-blue-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
{part.slice(1, -1)}
|
|
|
|
|
|
</q>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <React.Fragment key={index}>{part}</React.Fragment>
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const renderFormattedText = (value: string) => {
|
|
|
|
|
|
const text = value.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\r\n/g, '\n')
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-2 whitespace-pre-wrap leading-6">
|
|
|
|
|
|
{text.split('\n').map((line, index) => {
|
|
|
|
|
|
if (!line.trim()) return <div key={index} className="h-2" />
|
|
|
|
|
|
|
|
|
|
|
|
const listMarker = line.match(/^[-*+]\s+/)
|
|
|
|
|
|
const isListItem = Boolean(listMarker)
|
|
|
|
|
|
const content = isListItem ? line.slice(listMarker![0].length) : line
|
|
|
|
|
|
|
|
|
|
|
|
return isListItem ? (
|
|
|
|
|
|
<div key={index} className="flex gap-2">
|
|
|
|
|
|
<span className="text-blue-500 dark:text-blue-300">•</span>
|
|
|
|
|
|
<span>{renderInlineText(content)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p key={index}>{renderInlineText(content)}</p>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
// Main Component
|
|
|
|
|
|
const Assistant = () => {
|
|
|
|
|
|
// Hooks
|
2026-06-20 09:09:37 +00:00
|
|
|
|
const [conversations, setConversations] = useState<Conversation[]>([])
|
|
|
|
|
|
const [activeConversationId, setActiveConversationId] = useState<string | null>(null)
|
|
|
|
|
|
const [isConversationsLoaded, setIsConversationsLoaded] = useState(false)
|
|
|
|
|
|
const [isConversationDrawerOpen, setIsConversationDrawerOpen] = useState(false)
|
|
|
|
|
|
const [, setMessages] = useState<Message[]>([])
|
|
|
|
|
|
const aiPosts: Message[] = []
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const [input, setInput] = useState('')
|
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
2026-03-21 16:59:22 +00:00
|
|
|
|
const [bot, setBot] = useState<AiDto[]>([])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const [selectedBot, setSelectedBot] = useState<string | null>(null)
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
const { avatar } = useStoreState((state) => state.auth.user)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
|
|
|
|
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
const createConversation = (): Conversation => {
|
|
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: crypto.randomUUID(),
|
|
|
|
|
|
title: translate('::App.Platform.NewChat'),
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
createdAt: now,
|
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (messages.length === 0 && aiPosts.length > 0) {
|
|
|
|
|
|
setMessages(aiPosts) // artık doğrudan Message[]
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [aiPosts])
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
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('')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
// Botları çek
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchBots = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await aiService.getList({
|
|
|
|
|
|
skipCount: 0,
|
|
|
|
|
|
maxResultCount: 1000,
|
|
|
|
|
|
sorting: 'name',
|
|
|
|
|
|
})
|
2026-03-21 16:59:22 +00:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} 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()
|
2026-06-20 09:09:37 +00:00
|
|
|
|
if (!input.trim() || !activeConversation) return
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
const userMessage = input.trim()
|
2026-06-20 09:09:37 +00:00
|
|
|
|
const conversationId = activeConversation.id
|
2026-03-22 15:05:08 +00:00
|
|
|
|
const userMessageCreatedAt = new Date().toISOString()
|
2026-02-24 20:44:16 +00:00
|
|
|
|
setInput('')
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
|
|
|
|
|
|
// 1️⃣ Soruyu store'a ekle
|
2026-06-20 09:09:37 +00:00
|
|
|
|
appendMessage(conversationId, {
|
|
|
|
|
|
role: 'user',
|
|
|
|
|
|
content: userMessage,
|
|
|
|
|
|
createdAt: userMessageCreatedAt,
|
|
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-21 16:59:22 +00:00
|
|
|
|
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, {
|
2026-02-24 20:44:16 +00:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
2026-06-20 09:09:37 +00:00
|
|
|
|
biletId: conversationId,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
question: userMessage,
|
2026-06-20 09:09:37 +00:00
|
|
|
|
sessionId: conversationId,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-22 15:05:08 +00:00
|
|
|
|
const assistantMessageCreatedAt = new Date().toISOString()
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
appendMessage(conversationId, {
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: mapped,
|
|
|
|
|
|
createdAt: assistantMessageCreatedAt,
|
|
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
const errorMessage = 'Üzgünüm, bir hata oluştu. Lütfen tekrar deneyin.'
|
2026-03-22 15:05:08 +00:00
|
|
|
|
const assistantMessageCreatedAt = new Date().toISOString()
|
2026-06-20 09:09:37 +00:00
|
|
|
|
appendMessage(conversationId, {
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: errorMessage,
|
|
|
|
|
|
createdAt: assistantMessageCreatedAt,
|
|
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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<string, string> = {
|
2026-06-20 09:09:37 +00:00
|
|
|
|
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',
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
|
|
|
|
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 (
|
|
|
|
|
|
<div className="space-y-2 text-sm">
|
|
|
|
|
|
<div
|
2026-06-20 09:09:37 +00:00
|
|
|
|
className={`inline-block px-3 py-1 text-xs font-medium rounded-full mb-2 ${typeStyles[type] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200'}`}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
>
|
|
|
|
|
|
{typeLabels[type] || type}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{sql && (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded border border-gray-200 dark:border-gray-700 text-xs font-mono whitespace-pre-wrap">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<pre>
|
|
|
|
|
|
<code>{cleanedSql}</code>
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{error ? (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<div className="text-red-700 dark:text-red-200 bg-red-100 dark:bg-red-900/50 border border-red-300 dark:border-red-800 rounded p-2">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
⚠️ <strong>Hata:</strong> {error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : typeof answer === 'string' ? (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
renderFormattedText(answer)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
) : Array.isArray(answer) ? (
|
|
|
|
|
|
answer.length === 0 ? (
|
|
|
|
|
|
<div className="text-gray-500 italic">Sonuç bulunamadı.</div>
|
|
|
|
|
|
) : (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<div className="overflow-x-auto max-h-[400px] overflow-y-auto border border-gray-200 dark:border-gray-700 rounded">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<table className="table-auto w-full text-sm">
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<tr>
|
|
|
|
|
|
{Object.keys(answer[0]).map((col) => (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<th
|
|
|
|
|
|
key={col}
|
|
|
|
|
|
className="border border-gray-200 dark:border-gray-700 px-2 py-1 text-left"
|
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{col}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{answer.map((row, rowIndex) => (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<tr key={rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{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 (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<td
|
|
|
|
|
|
key={colIndex}
|
|
|
|
|
|
className="border border-gray-200 dark:border-gray-700 px-2 py-1"
|
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{display}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
) : typeof answer === 'object' && answer !== null && (answer as any).message ? (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
renderFormattedText(String((answer as any).message))
|
2026-02-24 20:44:16 +00:00
|
|
|
|
) : (
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded border border-gray-200 dark:border-gray-700">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{JSON.stringify(answer, null, 2)}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{chart && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={`data:image/png;base64,${chart}`}
|
|
|
|
|
|
alt="Grafik"
|
2026-06-20 09:09:37 +00:00
|
|
|
|
className="max-w-full rounded border border-gray-200 dark:border-gray-700"
|
2026-02-24 20:44:16 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
return message.role === 'assistant' ? (
|
|
|
|
|
|
<div className="text-sm">{renderFormattedText(String(message.content))}</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="whitespace-pre-wrap text-sm">{String(message.content)}</div>
|
|
|
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Container>
|
|
|
|
|
|
<Helmet
|
|
|
|
|
|
titleTemplate={`%s | ${APP_NAME}`}
|
2026-03-28 08:23:21 +00:00
|
|
|
|
title={translate('::' + AI_ASSISTANT)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
defaultTitle={APP_NAME}
|
|
|
|
|
|
></Helmet>
|
|
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<Drawer
|
|
|
|
|
|
isOpen={isConversationDrawerOpen}
|
|
|
|
|
|
onClose={() => setIsConversationDrawerOpen(false)}
|
|
|
|
|
|
onRequestClose={() => setIsConversationDrawerOpen(false)}
|
|
|
|
|
|
placement="left"
|
|
|
|
|
|
title={translate('::App.Platform.Chats')}
|
|
|
|
|
|
width={320}
|
|
|
|
|
|
bodyClass="p-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button block icon={<FaPlus />} onClick={handleNewConversation} disabled={loading}>
|
|
|
|
|
|
{translate('::App.Platform.NewChat')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<div className="mt-3 max-h-[calc(100vh-140px)] overflow-y-auto overscroll-contain">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{conversations.map((conversation) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={conversation.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleSelectConversation(conversation.id)}
|
|
|
|
|
|
className={`flex w-full items-center gap-2 rounded-lg px-3 py-3 text-left text-sm transition-colors ${
|
|
|
|
|
|
conversation.id === activeConversationId
|
|
|
|
|
|
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'
|
|
|
|
|
|
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaComment className="shrink-0" />
|
|
|
|
|
|
<span className="truncate">{conversation.title}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
|
aria-label={`${conversation.title} sohbetini sil`}
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
handleDeleteConversation(conversation.id)
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="ml-auto shrink-0 rounded p-1 text-gray-400 hover:bg-red-100 hover:text-red-600 dark:text-gray-500 dark:hover:bg-red-900/40 dark:hover:text-red-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTrash className="text-xs" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="h-[calc(100vh-140px)] pt-2 flex gap-4">
|
|
|
|
|
|
<aside className="hidden w-64 shrink-0 flex-col rounded-xl border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800 lg:flex">
|
|
|
|
|
|
<Button block icon={<FaPlus />} onClick={handleNewConversation} disabled={loading}>
|
|
|
|
|
|
{translate('::App.Platform.NewChat')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<ScrollBar autoHide className="mt-3 flex-1 min-h-0">
|
|
|
|
|
|
<div className="space-y-1 pr-2">
|
|
|
|
|
|
{conversations.map((conversation) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={conversation.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleSelectConversation(conversation.id)}
|
|
|
|
|
|
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
|
|
|
|
|
conversation.id === activeConversationId
|
|
|
|
|
|
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'
|
|
|
|
|
|
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaComment className="shrink-0" />
|
|
|
|
|
|
<span className="truncate">{conversation.title}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
|
aria-label={`${conversation.title} sohbetini sil`}
|
|
|
|
|
|
title="Sohbeti sil"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
handleDeleteConversation(conversation.id)
|
|
|
|
|
|
}}
|
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
handleDeleteConversation(conversation.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="ml-auto shrink-0 rounded p-1 text-gray-400 hover:bg-red-100 hover:text-red-600 dark:text-gray-500 dark:hover:bg-red-900/40 dark:hover:text-red-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTrash className="text-xs" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
</ScrollBar>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="min-w-0 flex flex-1 flex-col">
|
|
|
|
|
|
<div className="mb-2 flex shrink-0 gap-2 lg:hidden">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="flex-1"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
icon={<FaPlus />}
|
|
|
|
|
|
onClick={handleNewConversation}
|
|
|
|
|
|
disabled={loading}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
{translate('::App.Platform.NewChat')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="flex-1"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
icon={<FaComment />}
|
|
|
|
|
|
onClick={() => setIsConversationDrawerOpen(true)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{translate('::App.Platform.Chats')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ScrollBar
|
|
|
|
|
|
autoHide
|
|
|
|
|
|
autoHideTimeout={700}
|
|
|
|
|
|
autoHideDuration={200}
|
|
|
|
|
|
hideTracksWhenNotNeeded
|
|
|
|
|
|
className="flex-1 min-h-0 min-w-0"
|
|
|
|
|
|
renderView={({ style, ...props }) => (
|
|
|
|
|
|
<div {...props} style={{ ...style, overflowX: 'hidden' }} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
renderThumbVertical={({ style, ...props }) => (
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div
|
2026-06-20 09:09:37 +00:00
|
|
|
|
{...props}
|
|
|
|
|
|
className="rounded-full bg-gray-300/80 dark:bg-gray-600/80"
|
|
|
|
|
|
style={{ ...style, width: 6, right: 3 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="p-4 space-y-4">
|
|
|
|
|
|
{messages.length === 0 && (
|
|
|
|
|
|
<div className="text-center text-gray-500 dark:text-gray-400 mt-8">
|
|
|
|
|
|
<FaRobot className="w-12 h-12 mx-auto mb-4 text-gray-400 dark:text-gray-500" />
|
|
|
|
|
|
<p className="mt-2">{translate('::AI.Welcome')}</p>
|
|
|
|
|
|
<p className="text-lg font-medium">{translate('::AI.Name')}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{messages.map((msg, idx) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={idx}
|
|
|
|
|
|
className={`flex items-start gap-2.5 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center ${msg.role === 'user' ? 'bg-blue-500' : 'bg-gray-600'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{msg.role === 'user' ? (
|
|
|
|
|
|
<Avatar size={32} shape="circle" src={avatar} alt="avatar" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<FaRobot className="w-5 h-5 text-white" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`max-w-[80%] min-w-0 rounded-lg p-2 ${msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-white text-gray-800 dark:bg-gray-800 dark:text-gray-100'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
|
{renderMessageContent(msg)}
|
|
|
|
|
|
{msg.createdAt && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`text-[11px] leading-none ${msg.role === 'user' ? 'text-white/80 text-right' : 'text-gray-500 dark:text-gray-400 text-right'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{dayjs(msg.createdAt).format('DD.MM.YYYY HH:mm')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-22 15:05:08 +00:00
|
|
|
|
</div>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
</div>
|
2026-03-22 15:05:08 +00:00
|
|
|
|
</div>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
))}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
{loading && (
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-gray-500 dark:border-gray-400 border-t-transparent" />
|
|
|
|
|
|
{translate('::AI.WaitAnswer')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<div ref={bottomRef}></div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
</ScrollBar>
|
|
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="relative w-full mx-auto px-4">
|
|
|
|
|
|
<div className="relative w-full bg-[#2a2a2a] text-white rounded-3xl px-4 pt-4 pb-10 shadow-md">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
|
value={input}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
onChange={(e) => setInput(e.target.value)}
|
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
handleSubmit(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder={translate('::AI.Asking')}
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
className="w-full resize-none bg-transparent border-none outline-none text-base placeholder-gray-400"
|
|
|
|
|
|
/>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-06-20 09:09:37 +00:00
|
|
|
|
<div className="absolute bottom-1 left-2 flex items-center gap-2 text-sm">
|
|
|
|
|
|
<Dropdown
|
|
|
|
|
|
placement="top-start"
|
|
|
|
|
|
title={
|
|
|
|
|
|
bot.find((item) => item.id === selectedBot)?.name ||
|
|
|
|
|
|
translate('::AI.SelectModel')
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{bot
|
|
|
|
|
|
.filter((item) => !!item.id)
|
|
|
|
|
|
.map((item) => (
|
|
|
|
|
|
<Dropdown.Item key={item.id!} eventKey={item.id!} onSelect={onBotItemClick}>
|
|
|
|
|
|
{item.name}
|
|
|
|
|
|
</Dropdown.Item>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="absolute bottom-2 right-3 flex items-center gap-2">
|
|
|
|
|
|
{/* <button
|
2026-02-24 20:44:16 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Mic"
|
|
|
|
|
|
className="rounded-full h-8 w-8 flex items-center justify-center text-gray-400 hover:text-white"
|
|
|
|
|
|
>
|
|
|
|
|
|
🎤
|
2026-06-20 09:09:37 +00:00
|
|
|
|
</button> */}
|
|
|
|
|
|
<Button size="sm" variant="plain" type="submit" disabled={loading}>
|
|
|
|
|
|
<FaPaperPlane />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-06-20 09:09:37 +00:00
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</Container>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default Assistant
|