File Management ve AI Asistant güncellemeleri

This commit is contained in:
Sedat Öztürk 2026-03-22 18:05:08 +03:00
parent 17df35102d
commit 62f38a27a5
7 changed files with 133 additions and 117 deletions

View file

@ -25,17 +25,16 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private readonly BlobManager _blobContainer; private readonly BlobManager _blobContainer;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private const string FileMetadataSuffix = ".metadata.json";
private const string FolderMarkerSuffix = ".folder"; private const string FolderMarkerSuffix = ".folder";
private const string IndexFileName = "index.json"; private const string IndexFileName = "index.json";
// Protected system folders that cannot be deleted, renamed, or moved // Protected system folders that cannot be deleted, renamed, or moved
private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase)
{ {
BlobContainerNames.Avatar, // BlobContainerNames.Avatar,
BlobContainerNames.Import, // BlobContainerNames.Import,
BlobContainerNames.Note, // BlobContainerNames.Note,
BlobContainerNames.Intranet // BlobContainerNames.Intranet
}; };
public FileManagementAppService( public FileManagementAppService(
@ -119,16 +118,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var rootFolder = pathParts[0]; var rootFolder = pathParts[0];
var isProtected = ProtectedFolders.Contains(rootFolder); var isProtected = ProtectedFolders.Contains(rootFolder);
Logger.LogInformation($"IsProtectedFolder - Path: '{path}', RootFolder: '{rootFolder}', IsProtected: {isProtected}");
Logger.LogInformation($"Protected folders: {string.Join(", ", ProtectedFolders)}");
return isProtected; return isProtected;
} }
private void ValidateNotProtectedFolder(string id, string operation) private void ValidateNotProtectedFolder(string id, string operation)
{ {
var decodedPath = DecodeIdAsPath(id); var decodedPath = DecodeIdAsPath(id);
Logger.LogInformation($"ValidateNotProtectedFolder - ID: {id}, DecodedPath: {decodedPath}, Operation: {operation}");
if (IsProtectedFolder(decodedPath)) if (IsProtectedFolder(decodedPath))
{ {
@ -136,8 +131,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Logger.LogWarning($"Blocked {operation} operation on protected folder: {folderName}"); Logger.LogWarning($"Blocked {operation} operation on protected folder: {folderName}");
throw new UserFriendlyException($"Cannot {operation} system folder '{folderName}'. This folder is protected."); throw new UserFriendlyException($"Cannot {operation} system folder '{folderName}'. This folder is protected.");
} }
Logger.LogInformation($"Folder {decodedPath} is not protected, allowing {operation}");
} }
private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId, string tenantId) private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId, string tenantId)
@ -1124,7 +1117,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
// Decode the folderId to get the actual path // Decode the folderId to get the actual path
var decodedPath = DecodeIdAsPath(folderId); var decodedPath = DecodeIdAsPath(folderId);
Logger.LogInformation($"GetFolderPath - FolderId: {folderId}, DecodedPath: {decodedPath}");
// Split path into parts and build breadcrumb // Split path into parts and build breadcrumb
var pathParts = decodedPath.Split('/', StringSplitOptions.RemoveEmptyEntries); var pathParts = decodedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
@ -1136,8 +1128,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var pathUpToCurrent = string.Join("/", pathParts.Take(i + 1)); var pathUpToCurrent = string.Join("/", pathParts.Take(i + 1));
currentEncodedPath = EncodePathAsId(pathUpToCurrent); currentEncodedPath = EncodePathAsId(pathUpToCurrent);
Logger.LogInformation($"PathItem {i}: Name='{pathParts[i]}', Id='{currentEncodedPath}', PathUpToCurrent='{pathUpToCurrent}'");
pathItems.Add(new PathItemDto pathItems.Add(new PathItemDto
{ {
Id = currentEncodedPath, Id = currentEncodedPath,
@ -1145,7 +1135,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}); });
} }
Logger.LogInformation($"Returning {pathItems.Count} breadcrumb items");
return new FolderPathDto { Path = pathItems }; return new FolderPathDto { Path = pathItems };
} }

View file

@ -822,16 +822,6 @@
"RequiredPermissionName": "App.Menus.Manager", "RequiredPermissionName": "App.Menus.Manager",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Saas",
"Code": "App.Files",
"DisplayName": "App.Files",
"Order": 14,
"Url": "/admin/files",
"Icon": "FcFolder",
"RequiredPermissionName": "App.Files",
"IsDisabled": false
},
{ {
"ParentCode": "App.Saas", "ParentCode": "App.Saas",
"Code": "App.DeveloperKit", "Code": "App.DeveloperKit",
@ -1084,11 +1074,21 @@
"RequiredPermissionName": "App.Reports.ReportTemplates", "RequiredPermissionName": "App.Reports.ReportTemplates",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Administration",
"Code": "App.Files",
"DisplayName": "App.Files",
"Order": 5,
"Url": "/admin/files",
"Icon": "FcFolder",
"RequiredPermissionName": "App.Files",
"IsDisabled": false
},
{ {
"ParentCode": "App.Administration", "ParentCode": "App.Administration",
"Code": "App.Forum", "Code": "App.Forum",
"DisplayName": "App.Forum", "DisplayName": "App.Forum",
"Order": 5, "Order": 6,
"Url": "/admin/forum", "Url": "/admin/forum",
"Icon": "FcLink", "Icon": "FcLink",
"RequiredPermissionName": "App.ForumManagement.Publish", "RequiredPermissionName": "App.ForumManagement.Publish",

View file

@ -2233,51 +2233,6 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Saas",
"Name": "App.Files",
"ParentName": null,
"DisplayName": "App.Files",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Saas",
"Name": "App.Files.Create",
"ParentName": "App.Files",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Saas",
"Name": "App.Files.Update",
"ParentName": "App.Files",
"DisplayName": "Update",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Saas",
"Name": "App.Files.Delete",
"ParentName": "App.Files",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Saas",
"Name": "App.Files.Widget",
"ParentName": "App.Files",
"DisplayName": "Widget",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.ForumManagement", "Name": "App.ForumManagement",
@ -3484,6 +3439,42 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Files",
"ParentName": null,
"DisplayName": "App.Files",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Files.Create",
"ParentName": "App.Files",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Files.Update",
"ParentName": "App.Files",
"DisplayName": "Update",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Files.Delete",
"ParentName": "App.Files",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.ForumManagement.Publish", "Name": "App.ForumManagement.Publish",

View file

@ -2,7 +2,7 @@ import Tooltip from '@/components/ui/Tooltip'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission' import { usePermission } from '@/utils/hooks/usePermission'
import { FcAssistant, FcHeadset } from 'react-icons/fc' import { FcHeadset } from 'react-icons/fc'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
const AiAssistant = () => { const AiAssistant = () => {

View file

@ -21,4 +21,6 @@ export type MessageContent = string | BaseContent
export interface Message { export interface Message {
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: MessageContent content: MessageContent
/** ISO string */
createdAt?: string
} }

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
import { useStoreState } from '@/store'
import { import {
FaFolder, FaFolder,
FaCloudUploadAlt, FaCloudUploadAlt,
@ -43,6 +44,10 @@ import { APP_NAME } from '@/constants/app.constant'
const FileManager = () => { const FileManager = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
const authTenantId = useStoreState((state) => state.auth.tenant?.tenantId)
const authTenantName = useStoreState((state) => state.auth.tenant?.tenantName)
const isHostContext = !authTenantId
// State // State
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [items, setItems] = useState<FileItemType[]>([]) const [items, setItems] = useState<FileItemType[]>([])
@ -71,7 +76,7 @@ const FileManager = () => {
const [tenants, setTenants] = useState<TenantDto[]>([]) const [tenants, setTenants] = useState<TenantDto[]>([])
const [tenantsLoading, setTenantsLoading] = useState(false) const [tenantsLoading, setTenantsLoading] = useState(false)
const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string } | undefined>( const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string } | undefined>(
undefined, authTenantId ? { id: authTenantId, name: authTenantName || '' } : undefined,
) )
// Tracks mid-flight tenant change so the fetch effect doesn't fire with a stale folderId // Tracks mid-flight tenant change so the fetch effect doesn't fire with a stale folderId
const pendingTenantChange = useRef(false) const pendingTenantChange = useRef(false)
@ -96,8 +101,20 @@ const FileManager = () => {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (isHostContext) {
fetchTenants() fetchTenants()
}, [fetchTenants]) }
}, [fetchTenants, isHostContext])
// If user is in a tenant context, lock selection to that tenant.
useEffect(() => {
if (!authTenantId) return
setSelectedTenant((prev) => {
if (prev?.id === authTenantId) return prev
return { id: authTenantId, name: authTenantName || prev?.name || '' }
})
}, [authTenantId, authTenantName])
// Reset navigation when tenant changes // Reset navigation when tenant changes
useEffect(() => { useEffect(() => {
@ -124,20 +141,6 @@ const FileManager = () => {
} }
}) })
// console.log('Fetched items:', protectedItems)
// console.log(
// 'Protected folders check:',
// protectedItems.filter((item) => item.isReadOnly),
// )
// console.log(
// 'Folders with childCount:',
// protectedItems.filter((item) => item.type === 'folder').map(item => ({
// name: item.name,
// childCount: item.childCount,
// hasChildCount: 'childCount' in item,
// type: typeof item.childCount
// }))
// )
setItems(protectedItems) setItems(protectedItems)
} catch (error) { } catch (error) {
console.error('Failed to fetch items:', error) console.error('Failed to fetch items:', error)
@ -742,6 +745,7 @@ const FileManager = () => {
{/* Tenant Selector Row */} {/* Tenant Selector Row */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="text-gray-500 flex-shrink-0" /> <FaBuilding className="text-gray-500 flex-shrink-0" />
{isHostContext ? (
<Select <Select
size="xs" size="xs"
isLoading={tenantsLoading} isLoading={tenantsLoading}
@ -765,6 +769,14 @@ const FileManager = () => {
} }
}} }}
/> />
) : (
<div
className="text-sm font-medium text-gray-700 dark:text-gray-200 truncate max-w-[220px]"
title={authTenantName || selectedTenant?.name || ''}
>
{authTenantName || selectedTenant?.name || ''}
</div>
)}
</div> </div>
{/* File Operations */} {/* File Operations */}

View file

@ -9,6 +9,7 @@ import { AiDto } from '@/proxy/ai/models'
import { Container } from '@/components/shared' import { Container } from '@/components/shared'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant' import { APP_NAME } from '@/constants/app.constant'
import dayjs from 'dayjs'
// Types // Types
type ChatType = 'chat' | 'query' | 'analyze' type ChatType = 'chat' | 'query' | 'analyze'
@ -27,6 +28,7 @@ type MessageContent = string | BaseContent
interface Message { interface Message {
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: MessageContent content: MessageContent
createdAt?: string
} }
const isContentObject = (content: MessageContent): content is BaseContent => const isContentObject = (content: MessageContent): content is BaseContent =>
@ -102,13 +104,17 @@ const Assistant = () => {
if (!input.trim()) return if (!input.trim()) return
const userMessage = input.trim() const userMessage = input.trim()
const userMessageCreatedAt = new Date().toISOString()
setInput('') setInput('')
setLoading(true) setLoading(true)
// 1⃣ Soruyu store'a ekle // 1⃣ Soruyu store'a ekle
addAiPost({ role: 'user', content: userMessage }) addAiPost({ role: 'user', content: userMessage, createdAt: userMessageCreatedAt })
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]) setMessages((prev) => [
...prev,
{ role: 'user', content: userMessage, createdAt: userMessageCreatedAt },
])
try { try {
const selectedBotItem = bot.find((item) => item.id === selectedBot) const selectedBotItem = bot.find((item) => item.id === selectedBot)
@ -140,17 +146,24 @@ const Assistant = () => {
const formattedAnswer = const formattedAnswer =
typeof mapped.answer === 'string' ? mapped.answer : JSON.stringify(mapped.answer) typeof mapped.answer === 'string' ? mapped.answer : JSON.stringify(mapped.answer)
addAiPost({ role: 'assistant', content: mapped }) // mapped bir BaseContent const assistantMessageCreatedAt = new Date().toISOString()
setMessages((prev) => [...prev, { role: 'assistant', content: mapped }]) addAiPost({ role: 'assistant', content: mapped, createdAt: assistantMessageCreatedAt }) // mapped bir BaseContent
setMessages((prev) => [
...prev,
{ role: 'assistant', content: mapped, createdAt: assistantMessageCreatedAt },
])
} catch { } catch {
const errorMessage = 'Üzgünüm, bir hata oluştu. Lütfen tekrar deneyin.' const errorMessage = 'Üzgünüm, bir hata oluştu. Lütfen tekrar deneyin.'
addAiPost({ role: 'assistant', content: errorMessage }) const assistantMessageCreatedAt = new Date().toISOString()
addAiPost({ role: 'assistant', content: errorMessage, createdAt: assistantMessageCreatedAt })
setMessages((prev) => [ setMessages((prev) => [
...prev, ...prev,
{ {
role: 'assistant', role: 'assistant',
content: errorMessage, content: errorMessage,
createdAt: assistantMessageCreatedAt,
}, },
]) ])
} }
@ -312,7 +325,16 @@ const Assistant = () => {
<div <div
className={`max-w-[80%] rounded-lg p-2 ${msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-white text-gray-800'}`} className={`max-w-[80%] rounded-lg p-2 ${msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-white text-gray-800'}`}
> >
<div className="flex flex-col gap-1">
{renderMessageContent(msg)} {renderMessageContent(msg)}
{msg.createdAt && (
<div
className={`text-[11px] leading-none ${msg.role === 'user' ? 'text-white/80 text-right' : 'text-gray-500 text-right'}`}
>
{dayjs(msg.createdAt).format('DD.MM.YYYY HH:mm')}
</div>
)}
</div>
</div> </div>
</div> </div>
))} ))}