questions chats
This commit is contained in:
@@ -100,4 +100,7 @@ export const Pages = {
|
|||||||
wallet: {
|
wallet: {
|
||||||
transactions: "/wallet/transactions",
|
transactions: "/wallet/transactions",
|
||||||
},
|
},
|
||||||
|
aiChats: {
|
||||||
|
list: "/ai-chats/list",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-1
@@ -69,7 +69,8 @@
|
|||||||
"pagers": "پیجرها",
|
"pagers": "پیجرها",
|
||||||
"notifications": "تنظیمات اعلانات",
|
"notifications": "تنظیمات اعلانات",
|
||||||
"statistics": "گزارشات",
|
"statistics": "گزارشات",
|
||||||
"sliders": "اسلایدرها"
|
"sliders": "اسلایدرها",
|
||||||
|
"ai_chats": "چتهای AI"
|
||||||
},
|
},
|
||||||
"shipment": {
|
"shipment": {
|
||||||
"dineIn": "سرو در رستوران",
|
"dineIn": "سرو در رستوران",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -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} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user