questions chats

This commit is contained in:
hamid zarghami
2026-07-23 11:38:07 +03:30
parent 9f8877daf9
commit a6593ed0e8
11 changed files with 373 additions and 2 deletions
+3
View File
@@ -100,4 +100,7 @@ export const Pages = {
wallet: { wallet: {
transactions: "/wallet/transactions", transactions: "/wallet/transactions",
}, },
aiChats: {
list: "/ai-chats/list",
},
}; };
+2 -1
View File
@@ -69,7 +69,8 @@
"pagers": "پیجرها", "pagers": "پیجرها",
"notifications": "تنظیمات اعلانات", "notifications": "تنظیمات اعلانات",
"statistics": "گزارشات", "statistics": "گزارشات",
"sliders": "اسلایدرها" "sliders": "اسلایدرها",
"ai_chats": "چت‌های AI"
}, },
"shipment": { "shipment": {
"dineIn": "سرو در رستوران", "dineIn": "سرو در رستوران",
+140
View File
@@ -0,0 +1,140 @@
import { type FC, useMemo, useState } from "react";
import { EmptyWallet } from "iconsax-react";
import Table from "@/components/Table";
import Filters from "@/components/Filters";
import DefaulModal from "@/components/DefaulModal";
import { useGetAiChats } from "./hooks/useAiChatData";
import { useAiChatFilters } from "./hooks/useAiChatFilters";
import { getAiChatTableColumns } from "./components/AiChatTableColumns";
import { useAiChatFiltersFields } from "./components/AiChatFiltersFields";
import {
getAiChatAnswer,
getAiChatQuestion,
type AiChat,
} from "./types/Types";
import type { RowDataType } from "@/components/types/TableTypes";
import { formatPrice } from "@/helpers/func";
const AiChatsList: FC = () => {
const [selectedChat, setSelectedChat] = useState<AiChat | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const {
filters,
currentPage,
apiParams,
handleFiltersChange,
handlePageChange,
} = useAiChatFilters();
const { data: chatsData, isLoading } = useGetAiChats(apiParams);
const chats = chatsData?.data || [];
const totalPages = chatsData?.meta?.totalPages || 1;
const totalCost = chatsData?.meta?.totalCost;
const pageCost = useMemo(
() => chats.reduce((sum, chat) => sum + (chat.cost || 0), 0),
[chats]
);
const filterFields = useAiChatFiltersFields();
const columns = useMemo(
() =>
getAiChatTableColumns({
onViewAnswer: (chat) => {
setSelectedChat(chat);
setIsModalOpen(true);
},
}),
[]
);
const handleCloseModal = () => {
setIsModalOpen(false);
setSelectedChat(null);
};
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">لیست چتهای AI</h1>
</div>
<div className="mt-6 bg-[#F5F5F5] rounded-2xl px-6 py-4 flex flex-wrap items-center gap-6 w-fit">
<div className="flex items-center gap-3">
<EmptyWallet size={22} color="black" />
<div>
<div className="text-xs text-description">
{typeof totalCost === "number"
? "مجموع هزینه کل"
: "مجموع هزینه این صفحه"}
</div>
<div className="text-sm font-medium min-w-[100px]">
{formatPrice(typeof totalCost === "number" ? totalCost : pageCost)}
</div>
</div>
</div>
{typeof chatsData?.meta?.total === "number" && (
<div>
<div className="text-xs text-description">تعداد سوالات</div>
<div className="text-sm font-medium">
{chatsData.meta.total.toLocaleString("fa-IR")}
</div>
</div>
)}
</div>
<div className="mt-8">
<Filters
fields={filterFields}
onChange={handleFiltersChange}
initialValues={filters}
searchField="search"
/>
</div>
<Table<AiChat & RowDataType>
columns={columns}
data={chats as (AiChat & RowDataType)[]}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange,
}}
/>
<DefaulModal
open={isModalOpen}
close={handleCloseModal}
isHeader={true}
title_header="جزئیات چت"
>
<div className="mt-4 space-y-4">
<div>
<div className="text-xs text-description mb-1">سوال</div>
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
{(selectedChat && getAiChatQuestion(selectedChat)) || "-"}
</p>
</div>
<div>
<div className="text-xs text-description mb-1">پاسخ</div>
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
{(selectedChat && getAiChatAnswer(selectedChat)) || "-"}
</p>
</div>
{typeof selectedChat?.cost === "number" && (
<div>
<div className="text-xs text-description mb-1">هزینه</div>
<p className="text-sm text-foreground">
{formatPrice(selectedChat.cost)}
</p>
</div>
)}
</div>
</DefaulModal>
</div>
);
};
export default AiChatsList;
@@ -0,0 +1,15 @@
import { useMemo } from "react";
import type { FieldType } from "@/components/Filters";
export const useAiChatFiltersFields = (): FieldType[] => {
return useMemo(
() => [
{
type: "input",
name: "search",
placeholder: "جستجو در سوالات",
},
],
[]
);
};
@@ -0,0 +1,89 @@
import { Eye } from "iconsax-react";
import type { ColumnType } from "@/components/types/TableTypes";
import {
getAiChatAnswer,
getAiChatQuestion,
getAiChatTokens,
type AiChat,
} from "../types/Types";
import { formatOptionalDate, formatPrice } from "@/helpers/func";
interface GetAiChatTableColumnsProps {
onViewAnswer?: (chat: AiChat) => void;
}
export const getAiChatTableColumns = (
props?: GetAiChatTableColumnsProps
): ColumnType<AiChat>[] => {
return [
{
key: "question",
title: "سوال",
render: (item: AiChat) => {
const question = getAiChatQuestion(item);
return (
<div className="max-w-md truncate" title={question}>
{question || "-"}
</div>
);
},
},
{
key: "type",
title: "نوع",
render: (item: AiChat) => {
return item.type || "-";
},
},
{
key: "cost",
title: "هزینه مصرف‌شده",
render: (item: AiChat) => {
return typeof item.cost === "number" ? formatPrice(item.cost) : "-";
},
},
{
key: "consumedTokens",
title: "توکن مصرفی",
render: (item: AiChat) => {
const tokens = getAiChatTokens(item);
return typeof tokens === "number"
? tokens.toLocaleString("fa-IR")
: "-";
},
},
{
key: "createdAt",
title: "تاریخ",
render: (item: AiChat) => {
return formatOptionalDate(item.createdAt, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
},
},
{
key: "details",
title: "",
render: (item: AiChat) => {
if (!getAiChatAnswer(item) || !props?.onViewAnswer) {
return null;
}
return (
<Eye
size={20}
color="#8C90A3"
className="cursor-pointer hover:opacity-70 transition-opacity"
onClick={(e) => {
e.stopPropagation();
props.onViewAnswer?.(item);
}}
/>
);
},
},
];
};
+10
View File
@@ -0,0 +1,10 @@
import * as api from "../service/AiChatService";
import { useQuery } from "@tanstack/react-query";
import type { GetAiChatsParams } from "../types/Types";
export const useGetAiChats = (params?: GetAiChatsParams) => {
return useQuery({
queryKey: ["ai-chats", params],
queryFn: () => api.getAiChats(params),
});
};
@@ -0,0 +1,43 @@
import { useState, useMemo } from "react";
import type { FilterValues } from "@/components/Filters";
import type { GetAiChatsParams } from "../types/Types";
const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 10;
export const useAiChatFilters = () => {
const [filters, setFilters] = useState<FilterValues>({});
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const [limit] = useState(DEFAULT_LIMIT);
const apiParams = useMemo<GetAiChatsParams>(() => {
const params: GetAiChatsParams = {
page: currentPage,
limit,
};
if (filters.search) {
params.search = filters.search as string;
}
return params;
}, [filters, currentPage, limit]);
const handleFiltersChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(DEFAULT_PAGE);
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return {
filters,
currentPage,
apiParams,
handleFiltersChange,
handlePageChange,
limit,
};
};
@@ -0,0 +1,11 @@
import axios from "@/config/axios";
import type { GetAiChatsParams, GetAiChatsResponse } from "../types/Types";
export const getAiChats = async (
params?: GetAiChatsParams
): Promise<GetAiChatsResponse> => {
const { data } = await axios.get<GetAiChatsResponse>("/admin/ai/chats", {
params,
});
return data;
};
+47
View File
@@ -0,0 +1,47 @@
import type { IResponse } from "@/types/response.types";
import type { RowDataType } from "@/components/types/TableTypes";
export interface AiChat extends RowDataType {
id: string;
createdAt: string;
updatedAt?: string;
deletedAt?: string | null;
question?: string;
prompt?: string;
message?: string;
answer?: string | null;
response?: string | null;
cost?: number;
consumedTokens?: number;
totalTokens?: number;
promptTokens?: number;
completionTokens?: number;
type?: string | null;
}
export type PaginationMeta = {
total: number;
page: number;
limit: number;
totalPages: number;
totalCost?: number;
};
export type GetAiChatsParams = {
page?: number;
limit?: number;
search?: string;
};
export type GetAiChatsResponse = IResponse<AiChat[]> & {
meta: PaginationMeta;
};
export const getAiChatQuestion = (chat: AiChat): string =>
chat.question || chat.prompt || chat.message || "";
export const getAiChatAnswer = (chat: AiChat): string =>
chat.answer || chat.response || "";
export const getAiChatTokens = (chat: AiChat): number | undefined =>
chat.consumedTokens ?? chat.totalTokens ?? chat.promptTokens;
+2
View File
@@ -57,6 +57,7 @@ import UpdateUserGroup from '@/pages/customers/groups/Update'
import CampaignsList from '@/pages/customers/campaigns/List' import CampaignsList from '@/pages/customers/campaigns/List'
import CreateCampaign from '@/pages/customers/campaigns/Create' import CreateCampaign from '@/pages/customers/campaigns/Create'
import WalletTransactions from '@/pages/wallet/Transactions' import WalletTransactions from '@/pages/wallet/Transactions'
import AiChatsList from '@/pages/aiChats/List'
const MainRouter: FC = () => { const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore()
@@ -137,6 +138,7 @@ const MainRouter: FC = () => {
<Route path={Pages.statistics.list} element={<Statistics />} /> <Route path={Pages.statistics.list} element={<Statistics />} />
<Route path={Pages.wallet.transactions} element={<WalletTransactions />} /> <Route path={Pages.wallet.transactions} element={<WalletTransactions />} />
<Route path={Pages.aiChats.list} element={<AiChatsList />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+11 -1
View File
@@ -1,6 +1,6 @@
import { usePermissions } from '@/hooks/usePermissions' import { usePermissions } from '@/hooks/usePermissions'
import { PermissionEnum } from '@/pages/roles/enum/Enum' import { PermissionEnum } from '@/pages/roles/enum/Enum'
import { Calendar, Card, DocumentText, Element3, Gallery, Home2, Logout, NotificationBing, People, Security, SecurityUser, Setting2, SmsEdit, Star1, TicketDiscount, TruckFast } from 'iconsax-react' import { Calendar, Card, DocumentText, Element3, Gallery, Home2, Logout, MessageQuestion, NotificationBing, People, Security, SecurityUser, Setting2, SmsEdit, Star1, TicketDiscount, TruckFast } from 'iconsax-react'
import { type FC, useEffect } from 'react' import { type FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Skeleton from 'react-loading-skeleton' import Skeleton from 'react-loading-skeleton'
@@ -123,6 +123,16 @@ const SideBar: FC = () => {
/> />
)} )}
{checkPermission(PermissionEnum.MANAGE_FOODS) && (
<SideBarItem
icon={<MessageQuestion variant={isActive('ai-chats') ? 'Bold' : 'Outline'} color={isActive('ai-chats') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.ai_chats')}
isActive={isActive('ai-chats')}
link={Pages.aiChats.list}
activeName='ai-chats'
/>
)}
{checkPermission(PermissionEnum.MANAGE_SCHEDULES) && ( {checkPermission(PermissionEnum.MANAGE_SCHEDULES) && (
<SideBarItem <SideBarItem
icon={<Calendar variant={isActive('schedule') ? 'Bold' : 'Outline'} color={isActive('schedule') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />} icon={<Calendar variant={isActive('schedule') ? 'Bold' : 'Outline'} color={isActive('schedule') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}