AiBotAppService güncellemesi

This commit is contained in:
Sedat Öztürk 2026-06-23 20:30:19 +03:00
parent ab955483eb
commit 9186792a98
12 changed files with 189 additions and 119 deletions

View file

@ -8,5 +8,6 @@ public class AiBotDto : FullAuditedEntityDto<Guid>
public string Name { get; set; }
public string ApiUrl { get; set; }
public string Description { get; set; }
public string Tenants { get; set; }
}

View file

@ -42,24 +42,31 @@ public static class LookupQueryValues
$"ORDER BY \"{DisplayExpr}\";";
}
public static string TechnicalSkillsValues =
$"SELECT " +
$"\"Name\" AS \"Key\", " +
$"\"Name\" AS \"Name\" " +
$"FROM \"{FullNameTable(TableNameEnum.Skill)}\" " +
$"WHERE \"SkillTypeId\" IN (SELECT \"Id\" FROM \"{FullNameTable(TableNameEnum.SkillType)}\" WHERE \"Name\"='Technical Skills' ) " +
$"ORDER BY \"Name\"";
public static string TenantValues =
$"SELECT * FROM (" +
$"SELECT NULL AS \"Key\", 'Host' AS \"Name\" " +
$"UNION ALL " +
$"SELECT " +
$"\"AbpTenants\".\"Id\" AS \"Key\", " +
$"\"AbpTenants\".\"Name\" AS \"Name\" " +
$"FROM \"AbpTenants\"" +
$") AS \"List\" " +
$"ORDER BY \"Name\"";
public static string TenantValues(bool alwaysName = false)
{
if (alwaysName)
{
return $"SELECT * FROM (" +
$"SELECT " +
$"\"AbpTenants\".\"Name\" AS \"Key\", " +
$"\"AbpTenants\".\"Name\" AS \"Name\" " +
$"FROM \"AbpTenants\"" +
$") AS \"List\" " +
$"ORDER BY \"Name\"";
}
else
{
return $"SELECT * FROM (" +
$"SELECT NULL AS \"Key\", 'Host' AS \"Name\" " +
$"UNION ALL " +
$"SELECT " +
$"\"AbpTenants\".\"Id\" AS \"Key\", " +
$"\"AbpTenants\".\"Name\" AS \"Name\" " +
$"FROM \"AbpTenants\"" +
$") AS \"List\" " +
$"ORDER BY \"Name\"";
}
}
public static string LanguageKeyValues =
$"SELECT " +
@ -231,4 +238,13 @@ public static class LookupQueryValues
$"FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.SurveyQuestion))}\" " +
$"WHERE \"IsDeleted\" = 'false' " +
$"ORDER BY \"QuestionText\";";
public static string TechnicalSkillsValues =
$"SELECT " +
$"\"Name\" AS \"Key\", " +
$"\"Name\" AS \"Name\" " +
$"FROM \"{FullNameTable(TableNameEnum.Skill)}\" " +
$"WHERE \"SkillTypeId\" IN (SELECT \"Id\" FROM \"{FullNameTable(TableNameEnum.SkillType)}\" WHERE \"Name\"='Technical Skills' ) " +
$"ORDER BY \"Name\"";
}

View file

@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using System.Threading.Tasks;
using System.Linq;
namespace Sozsoft.Platform.AiBots;
@ -18,5 +20,29 @@ public class AiBotAppService : CrudAppService<
{
}
public override async Task<PagedResultDto<AiBotDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input);
if (CurrentTenant.IsAvailable && !string.IsNullOrWhiteSpace(CurrentTenant.Name))
{
var tenantName = CurrentTenant.Name;
query = query.Where(aiBot =>
string.IsNullOrWhiteSpace(aiBot.Tenants) ||
aiBot.Tenants.Contains($"\"{tenantName}\""));
}
var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var entities = await AsyncExecuter.ToListAsync(query);
var dtos = await MapToGetListOutputDtosAsync(entities);
return new PagedResultDto<AiBotDto>(totalCount, dtos);
}
}

View file

@ -15116,6 +15116,12 @@
"en": "Is Tenant",
"tr": "Tenant"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Tenants",
"en": "Tenants",
"tr": "Tenantlar"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsTransfer",

View file

@ -679,7 +679,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.TenantValues,
LookupQuery = LookupQueryValues.TenantValues(),
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
@ -1239,14 +1239,15 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 750, true, true, true, true, false),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 650, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
{
new() { Order=1, ColCount=2, ColSpan=1, ItemType="group", Items = [
new EditingFormItemDto { Order=1, DataField="Name", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=2, DataField="IsActive", ColSpan=1, EditorType2=EditorTypes.dxCheckBox },
new EditingFormItemDto { Order=3, DataField="ApiUrl", ColSpan=2, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=4, DataField="Description", ColSpan=2, EditorType2=EditorTypes.dxHtmlEditor, EditorOptions=EditorOptionValues.HtmlEditorOptions },
new EditingFormItemDto { Order=4, DataField="Tenants", ColSpan=2, EditorType2=EditorTypes.dxTagBox },
new EditingFormItemDto { Order=5, DataField="Description", ColSpan=2, EditorType2=EditorTypes.dxHtmlEditor, EditorOptions=EditorOptionValues.HtmlEditorOptions },
]}
}),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[]
@ -1337,6 +1338,28 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new ListFormField
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Tenants",
CaptionName = "App.Listform.ListformField.Tenants",
Width = 0,
ListOrderNo = 6,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.TenantValues(true),
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
]);
#endregion
}

View file

@ -6,7 +6,8 @@ namespace Sozsoft.Platform.Entities;
public class AiBot : Entity<Guid>
{
public string Name { get; set; }
public string Description { get; set; }
public string ApiUrl { get; set; }
public bool IsActive { get; set; }
public string ApiUrl { get; set; }
public string Description { get; set; }
public string Tenants { get; set; }
}

View file

@ -194,6 +194,7 @@ public class PlatformDbContext :
b.Property(a => a.Name).IsRequired().HasMaxLength(128);
b.Property(a => a.ApiUrl).HasMaxLength(256);
b.Property(a => a.Description).HasColumnType("text");
b.Property(a => a.Tenants).HasColumnType("text");
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter(null);
});

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260623105928_Initial")]
[Migration("20260623164647_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -699,6 +699,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Tenants")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")

View file

@ -921,9 +921,10 @@ namespace Sozsoft.Platform.Migrations
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ApiUrl = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false)
Description = table.Column<string>(type: "text", nullable: true),
Tenants = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{

View file

@ -696,6 +696,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Tenants")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")

View file

@ -4,17 +4,19 @@ export interface AiBotDto extends FullAuditedEntityDto<string> {
name: string
apiUrl?: string
description?: string
tenants?: string
}
// Types
export type ChatType = 'chat' | 'query' | 'analyze'
export interface BaseContent {
type: ChatType
question: string
sql: string | null
answer: string | any[]
sql?: string | null
answer?: string | any[] | null
chart?: string
error?: string
error?: string | null
}
export type MessageContent = string | BaseContent
@ -22,6 +24,13 @@ export type MessageContent = string | BaseContent
export interface Message {
role: 'user' | 'assistant'
content: MessageContent
/** ISO string */
createdAt?: string
}
export interface Conversation {
id: string
title: string
messages: Message[]
createdAt: string
updatedAt: string
}

View file

@ -4,41 +4,13 @@ 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 { AiBotDto } from '@/proxy/ai/models'
import { AiBotDto, BaseContent, Conversation, Message, MessageContent } 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'
@ -330,7 +302,6 @@ const Assistant = () => {
question: userMessage,
sessionId: conversationId,
tenantName: tenant.tenantName ?? null,
// description: selectedBotItem?.description?.replace(/\r?\n/g, '\\n'),
}),
})
@ -338,8 +309,12 @@ const Assistant = () => {
const raw = Array.isArray(data) ? data[0] : data
const mapped: BaseContent = {
...raw,
result: raw.result || raw.answer || raw.error || 'Sonuç bulunamadı.',
type: raw?.type || 'chat',
question: raw?.question || userMessage,
sql: raw?.sql ?? null,
answer: raw?.answer ?? null,
chart: raw?.chart,
error: raw?.error ?? null,
}
// 2⃣ Cevabı store'a ekle
@ -407,68 +382,73 @@ const Assistant = () => {
</div>
)}
{error ? (
{error && (
<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">
<strong>Hata:</strong> {error}
</div>
) : typeof answer === 'string' ? (
renderFormattedText(answer)
) : Array.isArray(answer) ? (
answer.length === 0 ? (
)}
{!error &&
(typeof answer === 'string' ? (
renderFormattedText(answer)
) : Array.isArray(answer) ? (
answer.length === 0 ? (
<div className="text-gray-500 italic">Sonuç bulunamadı.</div>
) : (
<div className="overflow-x-auto max-h-[400px] overflow-y-auto border border-gray-200 dark:border-gray-700 rounded">
<table className="table-auto w-full text-sm">
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
<tr>
{Object.keys(answer[0]).map((col) => (
<th
key={col}
className="border border-gray-200 dark:border-gray-700 px-2 py-1 text-left"
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{answer.map((row, rowIndex) => (
<tr key={rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
{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 (
<td
key={colIndex}
className="border border-gray-200 dark:border-gray-700 px-2 py-1"
>
{display}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
) : typeof answer === 'object' && answer !== null && (answer as any).message ? (
renderFormattedText(String((answer as any).message))
) : answer === null || answer === undefined ? (
<div className="text-gray-500 italic">Sonuç bulunamadı.</div>
) : (
<div className="overflow-x-auto max-h-[400px] overflow-y-auto border border-gray-200 dark:border-gray-700 rounded">
<table className="table-auto w-full text-sm">
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
<tr>
{Object.keys(answer[0]).map((col) => (
<th
key={col}
className="border border-gray-200 dark:border-gray-700 px-2 py-1 text-left"
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{answer.map((row, rowIndex) => (
<tr key={rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
{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 (
<td
key={colIndex}
className="border border-gray-200 dark:border-gray-700 px-2 py-1"
>
{display}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
) : typeof answer === 'object' && answer !== null && (answer as any).message ? (
renderFormattedText(String((answer as any).message))
) : (
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded border border-gray-200 dark:border-gray-700">
{JSON.stringify(answer, null, 2)}
</pre>
)}
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded border border-gray-200 dark:border-gray-700">
{JSON.stringify(answer, null, 2)}
</pre>
))}
{chart && (
<div className="mt-4">