From a6593ed0e8f9236e806ea91633370ac3099a97e0 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 23 Jul 2026 11:38:07 +0330 Subject: [PATCH] questions chats --- src/config/Pages.ts | 3 + src/langs/fa.json | 3 +- src/pages/aiChats/List.tsx | 140 ++++++++++++++++++ .../components/AiChatFiltersFields.tsx | 15 ++ .../aiChats/components/AiChatTableColumns.tsx | 89 +++++++++++ src/pages/aiChats/hooks/useAiChatData.ts | 10 ++ src/pages/aiChats/hooks/useAiChatFilters.ts | 43 ++++++ src/pages/aiChats/service/AiChatService.ts | 11 ++ src/pages/aiChats/types/Types.ts | 47 ++++++ src/router/Main.tsx | 2 + src/shared/SideBar.tsx | 12 +- 11 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 src/pages/aiChats/List.tsx create mode 100644 src/pages/aiChats/components/AiChatFiltersFields.tsx create mode 100644 src/pages/aiChats/components/AiChatTableColumns.tsx create mode 100644 src/pages/aiChats/hooks/useAiChatData.ts create mode 100644 src/pages/aiChats/hooks/useAiChatFilters.ts create mode 100644 src/pages/aiChats/service/AiChatService.ts create mode 100644 src/pages/aiChats/types/Types.ts diff --git a/src/config/Pages.ts b/src/config/Pages.ts index b58510d..fa74d37 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -100,4 +100,7 @@ export const Pages = { wallet: { transactions: "/wallet/transactions", }, + aiChats: { + list: "/ai-chats/list", + }, }; diff --git a/src/langs/fa.json b/src/langs/fa.json index 88a7c2f..d748ef3 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -69,7 +69,8 @@ "pagers": "پیجرها", "notifications": "تنظیمات اعلانات", "statistics": "گزارشات", - "sliders": "اسلایدرها" + "sliders": "اسلایدرها", + "ai_chats": "چت‌های AI" }, "shipment": { "dineIn": "سرو در رستوران", diff --git a/src/pages/aiChats/List.tsx b/src/pages/aiChats/List.tsx new file mode 100644 index 0000000..aa98bee --- /dev/null +++ b/src/pages/aiChats/List.tsx @@ -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(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 ( +
+
+

لیست چت‌های AI

+
+ +
+
+ +
+
+ {typeof totalCost === "number" + ? "مجموع هزینه کل" + : "مجموع هزینه این صفحه"} +
+
+ {formatPrice(typeof totalCost === "number" ? totalCost : pageCost)} +
+
+
+ {typeof chatsData?.meta?.total === "number" && ( +
+
تعداد سوالات
+
+ {chatsData.meta.total.toLocaleString("fa-IR")} +
+
+ )} +
+ +
+ +
+ + + columns={columns} + data={chats as (AiChat & RowDataType)[]} + isloading={isLoading} + pagination={{ + currentPage, + totalPages, + onPageChange: handlePageChange, + }} + /> + + +
+
+
سوال
+

+ {(selectedChat && getAiChatQuestion(selectedChat)) || "-"} +

+
+
+
پاسخ
+

+ {(selectedChat && getAiChatAnswer(selectedChat)) || "-"} +

+
+ {typeof selectedChat?.cost === "number" && ( +
+
هزینه
+

+ {formatPrice(selectedChat.cost)} +

+
+ )} +
+
+
+ ); +}; + +export default AiChatsList; diff --git a/src/pages/aiChats/components/AiChatFiltersFields.tsx b/src/pages/aiChats/components/AiChatFiltersFields.tsx new file mode 100644 index 0000000..bc2243e --- /dev/null +++ b/src/pages/aiChats/components/AiChatFiltersFields.tsx @@ -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: "جستجو در سوالات", + }, + ], + [] + ); +}; diff --git a/src/pages/aiChats/components/AiChatTableColumns.tsx b/src/pages/aiChats/components/AiChatTableColumns.tsx new file mode 100644 index 0000000..767374f --- /dev/null +++ b/src/pages/aiChats/components/AiChatTableColumns.tsx @@ -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[] => { + return [ + { + key: "question", + title: "سوال", + render: (item: AiChat) => { + const question = getAiChatQuestion(item); + return ( +
+ {question || "-"} +
+ ); + }, + }, + { + 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 ( + { + e.stopPropagation(); + props.onViewAnswer?.(item); + }} + /> + ); + }, + }, + ]; +}; diff --git a/src/pages/aiChats/hooks/useAiChatData.ts b/src/pages/aiChats/hooks/useAiChatData.ts new file mode 100644 index 0000000..1b6a2b3 --- /dev/null +++ b/src/pages/aiChats/hooks/useAiChatData.ts @@ -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), + }); +}; diff --git a/src/pages/aiChats/hooks/useAiChatFilters.ts b/src/pages/aiChats/hooks/useAiChatFilters.ts new file mode 100644 index 0000000..0d2508e --- /dev/null +++ b/src/pages/aiChats/hooks/useAiChatFilters.ts @@ -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({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const apiParams = useMemo(() => { + 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, + }; +}; diff --git a/src/pages/aiChats/service/AiChatService.ts b/src/pages/aiChats/service/AiChatService.ts new file mode 100644 index 0000000..b516964 --- /dev/null +++ b/src/pages/aiChats/service/AiChatService.ts @@ -0,0 +1,11 @@ +import axios from "@/config/axios"; +import type { GetAiChatsParams, GetAiChatsResponse } from "../types/Types"; + +export const getAiChats = async ( + params?: GetAiChatsParams +): Promise => { + const { data } = await axios.get("/admin/ai/chats", { + params, + }); + return data; +}; diff --git a/src/pages/aiChats/types/Types.ts b/src/pages/aiChats/types/Types.ts new file mode 100644 index 0000000..ebbe581 --- /dev/null +++ b/src/pages/aiChats/types/Types.ts @@ -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 & { + 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; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 1a7901f..26b5dec 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -57,6 +57,7 @@ import UpdateUserGroup from '@/pages/customers/groups/Update' import CampaignsList from '@/pages/customers/campaigns/List' import CreateCampaign from '@/pages/customers/campaigns/Create' import WalletTransactions from '@/pages/wallet/Transactions' +import AiChatsList from '@/pages/aiChats/List' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -137,6 +138,7 @@ const MainRouter: FC = () => { } /> } /> + } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index e6f8b95..8bfaa83 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -1,6 +1,6 @@ import { usePermissions } from '@/hooks/usePermissions' 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 { useTranslation } from 'react-i18next' import Skeleton from 'react-loading-skeleton' @@ -123,6 +123,16 @@ const SideBar: FC = () => { /> )} + {checkPermission(PermissionEnum.MANAGE_FOODS) && ( + } + title={t('sidebar.ai_chats')} + isActive={isActive('ai-chats')} + link={Pages.aiChats.list} + activeName='ai-chats' + /> + )} + {checkPermission(PermissionEnum.MANAGE_SCHEDULES) && ( }