diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json
index 4d77bfc..487fc47 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json
+++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json
@@ -11430,6 +11430,18 @@
"tr": "Yeni Sorgu",
"en": "New Query"
},
+ {
+ "resourceName": "Platform",
+ "key": "App.Platform.NewChat",
+ "tr": "Yeni Sohbet",
+ "en": "New Chat"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.Platform.Chats",
+ "tr": "Sohbetler",
+ "en": "Chats"
+ },
{
"resourceName": "Platform",
"key": "App.Platform.CopySelectedObjects",
diff --git a/ui/src/views/ai/Assistant.tsx b/ui/src/views/ai/Assistant.tsx
index 60fb3f6..7eb17bf 100644
--- a/ui/src/views/ai/Assistant.tsx
+++ b/ui/src/views/ai/Assistant.tsx
@@ -1,8 +1,7 @@
-import React, { useState, useRef, useEffect, SyntheticEvent } from 'react'
-import { FaRobot } from 'react-icons/fa'
-import { useStoreActions, useStoreState } from '@/store'
-import { Avatar, Dropdown } from '@/components/ui'
-import LoadAiPostsFromLocalStorage from './LoadAiPostsFromLocalStorage'
+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'
@@ -32,26 +31,98 @@ interface Message {
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 { addAiPost } = useStoreActions((actions) => actions.admin.messages)
- const [messages, setMessages] = useState([])
+ 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 { id, avatar } = useStoreState((state) => state.auth.user)
+ const { avatar } = useStoreState((state) => state.auth.user)
const inputRef = useRef(null)
const bottomRef = useRef(null)
const { translate } = useLocalization()
- const aiPosts = useStoreState((state) => state.admin.messages.aiPosts)
+ 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) {
@@ -59,6 +130,132 @@ const Assistant = () => {
}
}, [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 () => {
@@ -102,20 +299,20 @@ const Assistant = () => {
// Gönderme işlemi
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- if (!input.trim()) return
+ 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
- addAiPost({ role: 'user', content: userMessage, createdAt: userMessageCreatedAt })
-
- setMessages((prev) => [
- ...prev,
- { role: 'user', content: userMessage, createdAt: userMessageCreatedAt },
- ])
+ appendMessage(conversationId, {
+ role: 'user',
+ content: userMessage,
+ createdAt: userMessageCreatedAt,
+ })
try {
const selectedBotItem = bot.find((item) => item.id === selectedBot)
@@ -129,9 +326,9 @@ const Assistant = () => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- biletId: crypto.randomUUID(),
+ biletId: conversationId,
question: userMessage,
- sessionId: id,
+ sessionId: conversationId,
}),
})
@@ -144,29 +341,21 @@ const Assistant = () => {
}
// 2️⃣ Cevabı store'a ekle
- const formattedAnswer =
- typeof mapped.answer === 'string' ? mapped.answer : JSON.stringify(mapped.answer)
-
const assistantMessageCreatedAt = new Date().toISOString()
- addAiPost({ role: 'assistant', content: mapped, createdAt: assistantMessageCreatedAt }) // mapped bir BaseContent
-
- setMessages((prev) => [
- ...prev,
- { role: 'assistant', content: mapped, createdAt: assistantMessageCreatedAt },
- ])
+ 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()
- addAiPost({ role: 'assistant', content: errorMessage, createdAt: assistantMessageCreatedAt })
- setMessages((prev) => [
- ...prev,
- {
- role: 'assistant',
- content: errorMessage,
- createdAt: assistantMessageCreatedAt,
- },
- ])
+ appendMessage(conversationId, {
+ role: 'assistant',
+ content: errorMessage,
+ createdAt: assistantMessageCreatedAt,
+ })
}
setLoading(false)
@@ -180,9 +369,9 @@ const Assistant = () => {
}
const typeStyles: Record = {
- chat: 'bg-blue-100 text-blue-700',
- query: 'bg-green-100 text-green-700',
- analyze: 'bg-yellow-100 text-yellow-800',
+ 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 = {
@@ -203,13 +392,13 @@ const Assistant = () => {
return (
{typeLabels[type] || type}
{sql && (
-
+
{cleanedSql}
@@ -217,21 +406,24 @@ const Assistant = () => {
)}
{error ? (
-
+
⚠️ Hata: {error}
) : typeof answer === 'string' ? (
-
{answer}
+ renderFormattedText(answer)
) : Array.isArray(answer) ? (
answer.length === 0 ? (
Sonuç bulunamadı.
) : (
-
+
-
+
{Object.keys(answer[0]).map((col) => (
- |
+ |
{col}
|
))}
@@ -239,7 +431,7 @@ const Assistant = () => {
{answer.map((row, rowIndex) => (
-
+
{Object.keys(row).map((col, colIndex) => {
const val = row[col]
const display =
@@ -254,7 +446,10 @@ const Assistant = () => {
: String(val)
return (
- |
+ |
{display}
|
)
@@ -266,9 +461,9 @@ const Assistant = () => {
)
) : typeof answer === 'object' && answer !== null && (answer as any).message ? (
- {String((answer as any).message)}
+ renderFormattedText(String((answer as any).message))
) : (
-
+
{JSON.stringify(answer, null, 2)}
)}
@@ -278,7 +473,7 @@ const Assistant = () => {
)}
@@ -286,7 +481,11 @@ const Assistant = () => {
)
}
- return {String(message.content)}
+ return message.role === 'assistant' ? (
+ {renderFormattedText(String(message.content))}
+ ) : (
+ {String(message.content)}
+ )
}
// Render
@@ -298,110 +497,236 @@ const Assistant = () => {
defaultTitle={APP_NAME}
>
-
-
-
- {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')}
-
- )}
-
-
-
-
-