From a79429fdcde4a9f312070ee9ff1ac58144fe5088 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Wed, 17 May 2023 19:30:43 +0800 Subject: [PATCH] feat: kb crud --- public/docs/csvSelect.md | 12 +- src/api/model.ts | 68 +---- src/api/plugins/kb.ts | 73 ++++++ src/components/Icon/icons/kb.svg | 1 + src/components/Icon/index.tsx | 3 +- src/components/Layout/index.tsx | 26 +- src/components/Layout/navbar.tsx | 7 +- src/components/SideBar/index.tsx | 67 +++++ src/constants/kb.ts | 11 + src/constants/plugin.ts | 4 + src/hooks/usePagination.tsx | 1 + src/pages/api/model/data/fetchingUrlData.ts | 34 --- .../api/model/data/pushModelDataInput.ts | 54 ---- src/pages/api/model/del.ts | 6 - .../kb/delDataById.ts} | 0 .../kb/pushData.ts} | 59 ++--- .../kb/updateData.ts} | 2 +- .../text/splitText.ts} | 26 +- src/pages/api/plugins/kb/create.ts | 35 +++ .../kb}/data/exportModelData.ts | 10 +- .../kb/data/getDataList.ts} | 32 +-- .../kb}/data/getTrainingData.ts | 26 +- src/pages/api/plugins/kb/delete.ts | 43 +++ src/pages/api/plugins/kb/list.ts | 42 +++ src/pages/api/plugins/kb/update.ts | 39 +++ src/pages/chat/components/History.tsx | 6 +- src/pages/chat/components/ShareHistory.tsx | 6 +- src/pages/chat/index.tsx | 83 +----- src/pages/chat/share.tsx | 92 ++----- src/pages/index.tsx | 4 +- .../components/DataCard.tsx} | 200 +++++++------- src/pages/kb/components/Detail.tsx | 245 ++++++++++++++++++ .../components/InputDataModal.tsx | 20 +- src/pages/kb/components/KbList.tsx | 150 +++++++++++ .../components/SelectCsvModal.tsx | 28 +- .../components/SelectFileModal.tsx | 14 +- src/pages/kb/index.tsx | 43 +++ .../login/components/ForgetPasswordForm.tsx | 14 +- src/pages/login/components/LoginForm.tsx | 8 +- src/pages/login/components/RegisterForm.tsx | 14 +- src/pages/login/index.tsx | 12 +- src/pages/model/components/ModelList.tsx | 2 +- .../detail/components/SelectUrlModal.tsx | 162 ------------ src/pages/model/components/detail/index.tsx | 5 - src/pages/model/index.tsx | 18 +- src/service/errorCode.ts | 9 +- src/service/events/generateQA.ts | 9 +- src/service/models/kb.ts | 28 ++ src/service/models/splitData.ts | 6 +- src/service/mongo.ts | 1 + src/service/utils/auth.ts | 14 +- src/store/global.ts | 13 +- src/store/user.ts | 39 ++- src/types/model.d.ts | 9 - src/types/mongoSchema.d.ts | 21 +- src/types/pg.d.ts | 3 +- src/types/plugin.d.ts | 15 ++ 57 files changed, 1186 insertions(+), 788 deletions(-) create mode 100644 src/api/plugins/kb.ts create mode 100644 src/components/Icon/icons/kb.svg create mode 100644 src/components/SideBar/index.tsx create mode 100644 src/constants/kb.ts create mode 100644 src/constants/plugin.ts delete mode 100644 src/pages/api/model/data/fetchingUrlData.ts delete mode 100644 src/pages/api/model/data/pushModelDataInput.ts rename src/pages/api/{model/data/delModelDataById.ts => openapi/kb/delDataById.ts} (100%) rename src/pages/api/{model/data/pushModelDataCsv.ts => openapi/kb/pushData.ts} (67%) rename src/pages/api/{model/data/putModelData.ts => openapi/kb/updateData.ts} (94%) rename src/pages/api/{model/data/splitData.ts => openapi/text/splitText.ts} (72%) create mode 100644 src/pages/api/plugins/kb/create.ts rename src/pages/api/{model => plugins/kb}/data/exportModelData.ts (83%) rename src/pages/api/{model/data/getModelData.ts => plugins/kb/data/getDataList.ts} (67%) rename src/pages/api/{model => plugins/kb}/data/getTrainingData.ts (73%) create mode 100644 src/pages/api/plugins/kb/delete.ts create mode 100644 src/pages/api/plugins/kb/list.ts create mode 100644 src/pages/api/plugins/kb/update.ts rename src/pages/{model/components/detail/components/ModelDataCard.tsx => kb/components/DataCard.tsx} (61%) create mode 100644 src/pages/kb/components/Detail.tsx rename src/pages/{model/components/detail => kb}/components/InputDataModal.tsx (88%) create mode 100644 src/pages/kb/components/KbList.tsx rename src/pages/{model/components/detail => kb}/components/SelectCsvModal.tsx (89%) rename src/pages/{model/components/detail => kb}/components/SelectFileModal.tsx (96%) create mode 100644 src/pages/kb/index.tsx delete mode 100644 src/pages/model/components/detail/components/SelectUrlModal.tsx create mode 100644 src/service/models/kb.ts create mode 100644 src/types/plugin.d.ts diff --git a/public/docs/csvSelect.md b/public/docs/csvSelect.md index 1246cd699..94d8fe3e8 100644 --- a/public/docs/csvSelect.md +++ b/public/docs/csvSelect.md @@ -1,7 +1,9 @@ 接受一个 csv 文件,表格头包含 question 和 answer。question 代表问题,answer 代表答案。 -导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。 -**请保证 csv 文件为 utf-8 编码** -| question | answer | -| --- | --- | -| 什么是 laf | laf 是一个云函数开发平台…… | +导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。 + +### 请保证 csv 文件为 utf-8 编码 + +| question | answer | +| ------------- | ------------------------------------------------------ | +| 什么是 laf | laf 是一个云函数开发平台…… | | 什么是 sealos | Sealos 是以 kubernetes 为内核的云操作系统发行版,可以…… | diff --git a/src/api/model.ts b/src/api/model.ts index b0bcfc0e8..c9a6042b3 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,5 +1,5 @@ import { GET, POST, DELETE, PUT } from './request'; -import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema'; +import type { ModelSchema } from '@/types/mongoSchema'; import type { ModelUpdateParams, ShareModelItem } from '@/types/model'; import { RequestPaging } from '../types/index'; import { Obj2Query } from '@/utils/tools'; @@ -31,72 +31,6 @@ export const getModelById = (id: string) => GET(`/model/detail?mode export const putModelById = (id: string, data: ModelUpdateParams) => PUT(`/model/update?modelId=${id}`, data); -/* 模型 data */ -type GetModelDataListProps = RequestPaging & { - modelId: string; - searchText: string; -}; -/** - * 获取模型的知识库数据 - */ -export const getModelDataList = (props: GetModelDataListProps) => - GET(`/model/data/getModelData?${Obj2Query(props)}`); - -/** - * 获取导出数据(不分页) - */ -export const getExportDataList = (modelId: string) => - GET<[string, string][]>(`/model/data/exportModelData?modelId=${modelId}`); - -/** - * 获取模型正在拆分数据的数量 - */ -export const getModelSplitDataListLen = (modelId: string) => - GET<{ - splitDataQueue: number; - embeddingQueue: number; - }>(`/model/data/getTrainingData?modelId=${modelId}`); - -/** - * 获取 web 页面内容 - */ -export const getWebContent = (url: string) => POST(`/model/data/fetchingUrlData`, { url }); - -/** - * 手动输入数据 - */ -export const postModelDataInput = (data: { - modelId: string; - data: { a: ModelDataSchema['a']; q: ModelDataSchema['q'] }[]; -}) => POST(`/model/data/pushModelDataInput`, data); - -/** - * 拆分数据 - */ -export const postModelDataSplitData = (data: { - modelId: string; - chunks: string[]; - prompt: string; - mode: 'qa' | 'subsection'; -}) => POST(`/model/data/splitData`, data); - -/** - * json导入数据 - */ -export const postModelDataCsvData = (modelId: string, data: string[][]) => - POST(`/model/data/pushModelDataCsv`, { modelId, data: data }); - -/** - * 更新模型数据 - */ -export const putModelDataById = (data: { dataId: string; a: string; q?: string }) => - PUT('/model/data/putModelData', data); -/** - * 删除一条模型数据 - */ -export const delOneModelData = (dataId: string) => - DELETE(`/model/data/delModelDataById?dataId=${dataId}`); - /* 共享市场 */ /** * 获取共享市场模型 diff --git a/src/api/plugins/kb.ts b/src/api/plugins/kb.ts new file mode 100644 index 000000000..3ba66c424 --- /dev/null +++ b/src/api/plugins/kb.ts @@ -0,0 +1,73 @@ +import { GET, POST, PUT, DELETE } from '../request'; +import type { KbItemType } from '@/types/plugin'; +import { RequestPaging } from '@/types/index'; +import { SplitTextTypEnum } from '@/constants/plugin'; +import { KbDataItemType } from '@/types/plugin'; + +export type KbUpdateParams = { id: string; name: string; tags: string; avatar: string }; + +/* knowledge base */ +export const getKbList = () => GET(`/plugins/kb/list`); + +export const postCreateKb = (data: { name: string }) => POST(`/plugins/kb/create`, data); + +export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, data); + +export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`); + +/* kb data */ +type GetKbDataListProps = RequestPaging & { + kbId: string; + searchText: string; +}; +export const getKbDataList = (data: GetKbDataListProps) => + POST(`/plugins/kb/data/getDataList`, data); + +/** + * 获取导出数据(不分页) + */ +export const getExportDataList = (kbId: string) => + GET<[string, string][]>(`/plugins/kb/data/exportModelData?kbId=${kbId}`); + +/** + * 获取模型正在拆分数据的数量 + */ +export const getTrainingData = (kbId: string) => + GET<{ + splitDataQueue: number; + embeddingQueue: number; + }>(`/plugins/kb/data/getTrainingData?kbId=${kbId}`); + +/** + * 获取 web 页面内容 + */ +export const getWebContent = (url: string) => POST(`/model/data/fetchingUrlData`, { url }); + +/** + * 直接push数据 + */ +export const postKbDataFromList = (data: { + kbId: string; + data: { a: KbDataItemType['a']; q: KbDataItemType['q'] }[]; +}) => POST(`/openapi/kb/pushData`, data); + +/** + * 更新一条数据 + */ +export const putKbDataById = (data: { dataId: string; a: string; q?: string }) => + PUT('/openapi/kb/updateData', data); +/** + * 删除一条知识库数据 + */ +export const delOneKbDataByDataId = (dataId: string) => + DELETE(`/openapi/kb/delDataById?dataId=${dataId}`); + +/** + * 拆分数据 + */ +export const postSplitData = (data: { + kbId: string; + chunks: string[]; + prompt: string; + mode: `${SplitTextTypEnum}`; +}) => POST(`/openapi/text/splitText`, data); diff --git a/src/components/Icon/icons/kb.svg b/src/components/Icon/icons/kb.svg new file mode 100644 index 000000000..b5b7e8e86 --- /dev/null +++ b/src/components/Icon/icons/kb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index e6825ade2..571af2c18 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -26,7 +26,8 @@ const map = { closeSolid: require('./icons/closeSolid.svg').default, wx: require('./icons/wx.svg').default, out: require('./icons/out.svg').default, - git: require('./icons/git.svg').default + git: require('./icons/git.svg').default, + kb: require('./icons/kb.svg').default }; export type IconName = keyof typeof map; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index c5b4afe01..98df102c2 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useMemo } from 'react'; import { Box, useColorMode, Flex } from '@chakra-ui/react'; import { useRouter } from 'next/router'; -import { useScreen } from '@/hooks/useScreen'; import { useLoading } from '@/hooks/useLoading'; import { useGlobalStore } from '@/store/global'; +import { throttle } from 'lodash'; import Auth from './auth'; import Navbar from './navbar'; import NavbarPhone from './navbarPhone'; @@ -19,12 +19,11 @@ const phoneUnShowLayoutRoute: Record = { '/chat/share': true }; -const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => { - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); +const Layout = ({ children }: { children: JSX.Element }) => { const router = useRouter(); const { colorMode, setColorMode } = useColorMode(); const { Loading } = useLoading(); - const { loading } = useGlobalStore(); + const { loading, setScreenWidth, isPc } = useGlobalStore(); const isChatPage = useMemo( () => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0, @@ -37,6 +36,19 @@ const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: b } }, [colorMode, router.pathname, setColorMode]); + useEffect(() => { + const resize = throttle(() => { + setScreenWidth(document.documentElement.clientWidth); + }, 300); + resize(); + + window.addEventListener('resize', resize); + + return () => { + window.removeEventListener('resize', resize); + }; + }, [setScreenWidth]); + return ( <> { - return { - isPcDevice: !/Mobile/.test(req?.headers?.['user-agent']) - }; -}; diff --git a/src/components/Layout/navbar.tsx b/src/components/Layout/navbar.tsx index afd49b285..aa3633f32 100644 --- a/src/components/Layout/navbar.tsx +++ b/src/components/Layout/navbar.tsx @@ -22,13 +22,18 @@ const Navbar = () => { link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`, activeLink: ['/chat'] }, - { label: 'AI助手', icon: 'model', link: `/model?modelId=${lastModelId}`, activeLink: ['/model'] }, + { + label: '知识库', + icon: 'kb', + link: `/kb`, + activeLink: ['/kb'] + }, { label: '共享', icon: 'shareMarket', diff --git a/src/components/SideBar/index.tsx b/src/components/SideBar/index.tsx new file mode 100644 index 000000000..8345cbfaf --- /dev/null +++ b/src/components/SideBar/index.tsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import type { BoxProps } from '@chakra-ui/react'; +import MyIcon from '../Icon'; + +interface Props extends BoxProps {} + +const SideBar = (e?: Props) => { + const { + w = [1, '0 0 250px', '0 0 280px', '0 0 310px', '0 0 340px'], + children, + ...props + } = e || {}; + + const [foldSideBar, setFoldSideBar] = useState(false); + return ( + div': { visibility: 'visible', opacity: 1 } + }} + {...props} + > + setFoldSideBar(!foldSideBar)} + > + + + + {children} + + + ); +}; + +export default SideBar; diff --git a/src/constants/kb.ts b/src/constants/kb.ts new file mode 100644 index 000000000..9a1e2c1ec --- /dev/null +++ b/src/constants/kb.ts @@ -0,0 +1,11 @@ +import type { KbItemType } from '@/types/plugin'; + +export const defaultKbDetail: KbItemType = { + _id: '', + userId: '', + updateTime: new Date(), + avatar: '/icon/logo.png', + name: '', + tags: '', + totalData: 0 +}; diff --git a/src/constants/plugin.ts b/src/constants/plugin.ts new file mode 100644 index 000000000..83b301382 --- /dev/null +++ b/src/constants/plugin.ts @@ -0,0 +1,4 @@ +export enum SplitTextTypEnum { + 'qa' = 'qa', + 'subsection' = 'subsection' +} diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index 14447c8e1..6d30ddb30 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -41,6 +41,7 @@ export const usePagination = ({ }); console.log(error); } + return null; } }); diff --git a/src/pages/api/model/data/fetchingUrlData.ts b/src/pages/api/model/data/fetchingUrlData.ts deleted file mode 100644 index fdaf81e7d..000000000 --- a/src/pages/api/model/data/fetchingUrlData.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/service/response'; -import { connectToDatabase } from '@/service/mongo'; -import { authToken } from '@/service/utils/auth'; -import axios from 'axios'; -import { axiosConfig } from '@/service/utils/tools'; - -/** - * 读取网站的内容 - */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const { url } = req.body as { url: string }; - if (!url) { - throw new Error('缺少 url'); - } - await connectToDatabase(); - - await authToken(req); - - const data = await axios - .get(url, { - httpsAgent: axiosConfig().httpsAgent - }) - .then((res) => res.data as string); - - jsonRes(res, { data }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/src/pages/api/model/data/pushModelDataInput.ts b/src/pages/api/model/data/pushModelDataInput.ts deleted file mode 100644 index 64a8463a6..000000000 --- a/src/pages/api/model/data/pushModelDataInput.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/service/response'; -import { connectToDatabase } from '@/service/mongo'; -import { authToken } from '@/service/utils/auth'; -import { ModelDataSchema } from '@/types/mongoSchema'; -import { generateVector } from '@/service/events/generateVector'; -import { PgClient } from '@/service/pg'; -import { authModel } from '@/service/utils/auth'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const { modelId, data } = req.body as { - modelId: string; - data: { a: ModelDataSchema['a']; q: ModelDataSchema['q'] }[]; - }; - - if (!modelId || !Array.isArray(data)) { - throw new Error('缺少参数'); - } - - // 凭证校验 - const userId = await authToken(req); - - await connectToDatabase(); - - // 验证是否是该用户的 model - await authModel({ - userId, - modelId - }); - - // 插入记录 - await PgClient.insert('modelData', { - values: data.map((item) => [ - { key: 'user_id', value: userId }, - { key: 'model_id', value: modelId }, - { key: 'q', value: item.q }, - { key: 'a', value: item.a }, - { key: 'status', value: 'waiting' } - ]) - }); - - generateVector(); - - jsonRes(res, { - data: 0 - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/src/pages/api/model/del.ts b/src/pages/api/model/del.ts index 92796f9b2..8fb878e5b 100644 --- a/src/pages/api/model/del.ts +++ b/src/pages/api/model/del.ts @@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/service/response'; import { Chat, Model, connectToDatabase, Collection, ShareChat } from '@/service/mongo'; import { authToken } from '@/service/utils/auth'; -import { PgClient } from '@/service/pg'; import { authModel } from '@/service/utils/auth'; /* 获取我的模型 */ @@ -25,11 +24,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< userId }); - // 删除 pg 中所有该模型的数据 - await PgClient.delete('modelData', { - where: [['user_id', userId], 'AND', ['model_id', modelId]] - }); - // 删除对应的聊天 await Chat.deleteMany({ modelId diff --git a/src/pages/api/model/data/delModelDataById.ts b/src/pages/api/openapi/kb/delDataById.ts similarity index 100% rename from src/pages/api/model/data/delModelDataById.ts rename to src/pages/api/openapi/kb/delDataById.ts diff --git a/src/pages/api/model/data/pushModelDataCsv.ts b/src/pages/api/openapi/kb/pushData.ts similarity index 67% rename from src/pages/api/model/data/pushModelDataCsv.ts rename to src/pages/api/openapi/kb/pushData.ts index 9a736280b..9d0d12e62 100644 --- a/src/pages/api/model/data/pushModelDataCsv.ts +++ b/src/pages/api/openapi/kb/pushData.ts @@ -1,20 +1,25 @@ import type { NextApiRequest, NextApiResponse } from 'next'; +import type { KbDataItemType } from '@/types/plugin'; import { jsonRes } from '@/service/response'; import { connectToDatabase } from '@/service/mongo'; import { authToken } from '@/service/utils/auth'; import { generateVector } from '@/service/events/generateVector'; -import { ModelDataStatusEnum } from '@/constants/model'; import { PgClient } from '@/service/pg'; -import { authModel } from '@/service/utils/auth'; +import { authKb } from '@/service/utils/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { modelId, data } = req.body as { - modelId: string; - data: string[][]; + const { + kbId, + data, + formatLineBreak = true + } = req.body as { + kbId: string; + formatLineBreak?: boolean; + data: { a: KbDataItemType['a']; q: KbDataItemType['q'] }[]; }; - if (!modelId || !Array.isArray(data)) { + if (!kbId || !Array.isArray(data)) { throw new Error('缺少参数'); } @@ -23,31 +28,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await connectToDatabase(); - // 验证是否是该用户的 model - await authModel({ + await authKb({ userId, - modelId + kbId }); - // 去重 + // 过滤重复的内容 const searchRes = await Promise.allSettled( - data.map(async ([q, a = '']) => { + data.map(async ({ q, a = '' }) => { if (!q) { return Promise.reject('q为空'); } - try { + + if (formatLineBreak) { q = q.replace(/\\n/g, '\n'); a = a.replace(/\\n/g, '\n'); + } + + // Exactly the same data, not push + try { const count = await PgClient.count('modelData', { - where: [ - ['user_id', userId], - 'AND', - ['model_id', modelId], - 'AND', - ['q', q], - 'AND', - ['a', a] - ] + where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['q', q], 'AND', ['a', a]] }); if (count > 0) { return Promise.reject('已经存在'); @@ -61,25 +62,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); }) ); - // 过滤重复的内容 const filterData = searchRes .filter((item) => item.status === 'fulfilled') .map<{ q: string; a: string }>((item: any) => item.value); - // 插入 pg + // 插入记录 const insertRes = await PgClient.insert('modelData', { values: filterData.map((item) => [ { key: 'user_id', value: userId }, - { key: 'model_id', value: modelId }, + { key: 'kb_id', value: kbId }, { key: 'q', value: item.q }, { key: 'a', value: item.a }, - { key: 'status', value: ModelDataStatusEnum.waiting } + { key: 'status', value: 'waiting' } ]) }); generateVector(); jsonRes(res, { + message: `共插入 ${insertRes.rowCount} 条数据`, data: insertRes.rowCount }); } catch (err) { @@ -89,11 +90,3 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } } - -export const config = { - api: { - bodyParser: { - sizeLimit: '100mb' - } - } -}; diff --git a/src/pages/api/model/data/putModelData.ts b/src/pages/api/openapi/kb/updateData.ts similarity index 94% rename from src/pages/api/model/data/putModelData.ts rename to src/pages/api/openapi/kb/updateData.ts index b28e41ea8..8750db26b 100644 --- a/src/pages/api/model/data/putModelData.ts +++ b/src/pages/api/openapi/kb/updateData.ts @@ -16,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< // 凭证校验 const userId = await authToken(req); - // 更新 pg 内容 + // 更新 pg 内容.仅修改a,不需要更新向量。 await PgClient.update('modelData', { where: [['id', dataId], 'AND', ['user_id', userId]], values: [ diff --git a/src/pages/api/model/data/splitData.ts b/src/pages/api/openapi/text/splitText.ts similarity index 72% rename from src/pages/api/model/data/splitData.ts rename to src/pages/api/openapi/text/splitText.ts index 4ac3bde10..ea1241e8d 100644 --- a/src/pages/api/model/data/splitData.ts +++ b/src/pages/api/openapi/text/splitText.ts @@ -1,21 +1,22 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/service/response'; import { connectToDatabase, SplitData } from '@/service/mongo'; -import { authModel, authToken } from '@/service/utils/auth'; +import { authKb, authToken } from '@/service/utils/auth'; import { generateVector } from '@/service/events/generateVector'; import { generateQA } from '@/service/events/generateQA'; import { PgClient } from '@/service/pg'; +import { SplitTextTypEnum } from '@/constants/plugin'; -/* 拆分数据成QA */ +/* split text */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { chunks, modelId, prompt, mode } = req.body as { - modelId: string; + const { chunks, kbId, prompt, mode } = req.body as { + kbId: string; chunks: string[]; prompt: string; - mode: 'qa' | 'subsection'; + mode: `${SplitTextTypEnum}`; }; - if (!chunks || !modelId || !prompt) { + if (!chunks || !kbId || !prompt) { throw new Error('参数错误'); } await connectToDatabase(); @@ -23,27 +24,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const userId = await authToken(req); // 验证是否是该用户的 model - await authModel({ - modelId, + await authKb({ + kbId, userId }); - if (mode === 'qa') { + if (mode === SplitTextTypEnum.qa) { // 批量QA拆分插入数据 await SplitData.create({ userId, - modelId, + kbId, textList: chunks, prompt }); generateQA(); - } else if (mode === 'subsection') { + } else if (mode === SplitTextTypEnum.subsection) { + // 待优化,直接调用另一个接口 // 插入记录 await PgClient.insert('modelData', { values: chunks.map((item) => [ { key: 'user_id', value: userId }, - { key: 'model_id', value: modelId }, + { key: 'kb_id', value: kbId }, { key: 'q', value: item }, { key: 'a', value: '' }, { key: 'status', value: 'waiting' } diff --git a/src/pages/api/plugins/kb/create.ts b/src/pages/api/plugins/kb/create.ts new file mode 100644 index 000000000..be1b276b9 --- /dev/null +++ b/src/pages/api/plugins/kb/create.ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, KB } from '@/service/mongo'; +import { authToken } from '@/service/utils/auth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { name, tags } = req.body as { + name: string; + tags: string[]; + }; + + if (!name) { + throw new Error('缺少参数'); + } + + // 凭证校验 + const userId = await authToken(req); + + await connectToDatabase(); + + const { _id } = await KB.create({ + name, + userId, + tags + }); + + jsonRes(res, { data: _id }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/model/data/exportModelData.ts b/src/pages/api/plugins/kb/data/exportModelData.ts similarity index 83% rename from src/pages/api/model/data/exportModelData.ts rename to src/pages/api/plugins/kb/data/exportModelData.ts index 0cfb3027f..20805fbab 100644 --- a/src/pages/api/model/data/exportModelData.ts +++ b/src/pages/api/plugins/kb/data/exportModelData.ts @@ -6,11 +6,11 @@ import { PgClient } from '@/service/pg'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - let { modelId } = req.query as { - modelId: string; + let { kbId } = req.query as { + kbId: string; }; - if (!modelId) { + if (!kbId) { throw new Error('缺少参数'); } @@ -21,11 +21,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< // 统计数据 const count = await PgClient.count('modelData', { - where: [['model_id', modelId], 'AND', ['user_id', userId]] + where: [['kb_id', kbId], 'AND', ['user_id', userId]] }); // 从 pg 中获取所有数据 const pgData = await PgClient.select<{ q: string; a: string }>('modelData', { - where: [['model_id', modelId], 'AND', ['user_id', userId]], + where: [['kb_id', kbId], 'AND', ['user_id', userId]], fields: ['q', 'a'], order: [{ field: 'id', mode: 'DESC' }], limit: count diff --git a/src/pages/api/model/data/getModelData.ts b/src/pages/api/plugins/kb/data/getDataList.ts similarity index 67% rename from src/pages/api/model/data/getModelData.ts rename to src/pages/api/plugins/kb/data/getDataList.ts index 682dd843d..69314a063 100644 --- a/src/pages/api/model/data/getModelData.ts +++ b/src/pages/api/plugins/kb/data/getDataList.ts @@ -3,27 +3,22 @@ import { jsonRes } from '@/service/response'; import { connectToDatabase } from '@/service/mongo'; import { authToken } from '@/service/utils/auth'; import { PgClient } from '@/service/pg'; -import type { PgModelDataItemType } from '@/types/pg'; -import { authModel } from '@/service/utils/auth'; +import type { PgKBDataItemType } from '@/types/pg'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { let { - modelId, + kbId, pageNum = 1, pageSize = 10, searchText = '' - } = req.query as { - modelId: string; - pageNum: string; - pageSize: string; + } = req.body as { + kbId: string; + pageNum: number; + pageSize: number; searchText: string; }; - - pageNum = +pageNum; - pageSize = +pageSize; - - if (!modelId) { + if (!kbId) { throw new Error('缺少参数'); } @@ -32,19 +27,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await connectToDatabase(); - const { model } = await authModel({ - userId, - modelId, - authOwner: false - }); - const where: any = [ - ...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']), - ['model_id', modelId], + ['user_id', userId], + 'AND', + ['kb_id', kbId], ...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : []) ]; - const searchRes = await PgClient.select('modelData', { + const searchRes = await PgClient.select('modelData', { fields: ['id', 'q', 'a', 'status'], where, order: [{ field: 'id', mode: 'DESC' }], diff --git a/src/pages/api/model/data/getTrainingData.ts b/src/pages/api/plugins/kb/data/getTrainingData.ts similarity index 73% rename from src/pages/api/model/data/getTrainingData.ts rename to src/pages/api/plugins/kb/data/getTrainingData.ts index e95aa5c86..f463f8a42 100644 --- a/src/pages/api/model/data/getTrainingData.ts +++ b/src/pages/api/plugins/kb/data/getTrainingData.ts @@ -8,8 +8,8 @@ import { PgClient } from '@/service/pg'; /* 拆分数据成QA */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { modelId } = req.query as { modelId: string }; - if (!modelId) { + const { kbId } = req.query as { kbId: string }; + if (!kbId) { throw new Error('参数错误'); } await connectToDatabase(); @@ -19,25 +19,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // split queue data const data = await SplitData.find({ userId, - modelId, + kbId, textList: { $exists: true, $not: { $size: 0 } } }); // embedding queue data - const where: any = [ - ['user_id', userId], - 'AND', - ['model_id', modelId], - 'AND', - ['status', ModelDataStatusEnum.waiting] - ]; + const embeddingData = await PgClient.count('modelData', { + where: [ + ['user_id', userId], + 'AND', + ['kb_id', kbId], + 'AND', + ['status', ModelDataStatusEnum.waiting] + ] + }); jsonRes(res, { data: { splitDataQueue: data.map((item) => item.textList).flat().length, - embeddingQueue: await PgClient.count('modelData', { - where - }) + embeddingQueue: embeddingData } }); } catch (err) { diff --git a/src/pages/api/plugins/kb/delete.ts b/src/pages/api/plugins/kb/delete.ts new file mode 100644 index 000000000..300550535 --- /dev/null +++ b/src/pages/api/plugins/kb/delete.ts @@ -0,0 +1,43 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, KB } from '@/service/mongo'; +import { authToken } from '@/service/utils/auth'; +import { PgClient } from '@/service/pg'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { id } = req.query as { + id: string; + }; + + if (!id) { + throw new Error('缺少参数'); + } + + // 凭证校验 + const userId = await authToken(req); + + await connectToDatabase(); + + // delete mongo data + await KB.findOneAndDelete({ + _id: id, + userId + }); + + // delete all pg data + // 删除 pg 中所有该模型的数据 + await PgClient.delete('modelData', { + where: [['user_id', userId], 'AND', ['kb_id', id]] + }); + + // delete related model + + jsonRes(res); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/plugins/kb/list.ts b/src/pages/api/plugins/kb/list.ts new file mode 100644 index 000000000..bb137390c --- /dev/null +++ b/src/pages/api/plugins/kb/list.ts @@ -0,0 +1,42 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, KB } from '@/service/mongo'; +import { authToken } from '@/service/utils/auth'; +import { PgClient } from '@/service/pg'; +import { KbItemType } from '@/types/plugin'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + // 凭证校验 + const userId = await authToken(req); + + await connectToDatabase(); + + const kbList = await KB.find({ + userId + }).sort({ updateTime: -1 }); + + const data = await Promise.all( + kbList.map(async (item) => ({ + _id: item._id, + avatar: item.avatar, + name: item.name, + userId: item.userId, + updateTime: item.updateTime, + tags: item.tags.join(' '), + totalData: await PgClient.count('modelData', { + where: [['user_id', userId], 'AND', ['kb_id', item._id]] + }) + })) + ); + + jsonRes(res, { + data + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/plugins/kb/update.ts b/src/pages/api/plugins/kb/update.ts new file mode 100644 index 000000000..433072aa0 --- /dev/null +++ b/src/pages/api/plugins/kb/update.ts @@ -0,0 +1,39 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, KB } from '@/service/mongo'; +import { authToken } from '@/service/utils/auth'; +import type { KbUpdateParams } from '@/api/plugins/kb'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { id, name, tags, avatar } = req.body as KbUpdateParams; + + if (!id || !name) { + throw new Error('缺少参数'); + } + + // 凭证校验 + const userId = await authToken(req); + + await connectToDatabase(); + + await KB.findOneAndUpdate( + { + _id: id, + userId + }, + { + avatar, + name, + tags: tags.split(' ').filter((item) => item) + } + ); + + jsonRes(res); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/chat/components/History.tsx b/src/pages/chat/components/History.tsx index d58ea0e32..9e81f1c11 100644 --- a/src/pages/chat/components/History.tsx +++ b/src/pages/chat/components/History.tsx @@ -20,24 +20,22 @@ import { formatTimeToChatTime } from '@/utils/tools'; import MyIcon from '@/components/Icon'; import type { HistoryItemType, ExportChatType } from '@/types/chat'; import { useChatStore } from '@/store/chat'; -import { useScreen } from '@/hooks/useScreen'; import ModelList from './ModelList'; +import { useGlobalStore } from '@/store/global'; import styles from '../index.module.scss'; const PcSliderBar = ({ - isPcDevice, onclickDelHistory, onclickExportChat }: { - isPcDevice: boolean; onclickDelHistory: (historyId: string) => Promise; onclickExportChat: (type: ExportChatType) => void; }) => { const router = useRouter(); const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string }; const theme = useTheme(); - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { isPc } = useGlobalStore(); const ContextMenuRef = useRef(null); diff --git a/src/pages/chat/components/ShareHistory.tsx b/src/pages/chat/components/ShareHistory.tsx index 3c1c960bc..7aa7900b2 100644 --- a/src/pages/chat/components/ShareHistory.tsx +++ b/src/pages/chat/components/ShareHistory.tsx @@ -17,17 +17,15 @@ import { formatTimeToChatTime } from '@/utils/tools'; import MyIcon from '@/components/Icon'; import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat'; import { useChatStore } from '@/store/chat'; -import { useScreen } from '@/hooks/useScreen'; +import { useGlobalStore } from '@/store/global'; import styles from '../index.module.scss'; const PcSliderBar = ({ - isPcDevice, onclickDelHistory, onclickExportChat, onCloseSlider }: { - isPcDevice: boolean; onclickDelHistory: (historyId: string) => void; onclickExportChat: (type: ExportChatType) => void; onCloseSlider: () => void; @@ -35,7 +33,7 @@ const PcSliderBar = ({ const router = useRouter(); const { shareId = '', historyId = '' } = router.query as { shareId: string; historyId: string }; const theme = useTheme(); - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { isPc } = useGlobalStore(); const ContextMenuRef = useRef(null); diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index debbf53cf..b1c1ac774 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -33,7 +33,7 @@ import { useTheme } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; -import { useScreen } from '@/hooks/useScreen'; +import { useGlobalStore } from '@/store/global'; import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools'; @@ -50,6 +50,7 @@ import { htmlTemplate } from '@/constants/common'; import { useUserStore } from '@/store/user'; import Loading from '@/components/Loading'; import Markdown from '@/components/Markdown'; +import SideBar from '@/components/SideBar'; import Empty from './components/Empty'; const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), { @@ -64,15 +65,7 @@ import styles from './index.module.scss'; const textareaMinH = '22px'; -const Chat = ({ - modelId, - chatId, - isPcDevice -}: { - modelId: string; - chatId: string; - isPcDevice: boolean; -}) => { +const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { const router = useRouter(); const theme = useTheme(); @@ -92,7 +85,6 @@ const Chat = ({ top: number; message: ChatSiteItemType; }>(); - const [foldSliderBar, setFoldSlideBar] = useState(false); const { lastChatModelId, @@ -113,7 +105,7 @@ const Chat = ({ const { toast } = useToast(); const { copyData } = useCopyData(); - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { isPc } = useGlobalStore(); const { Loading, setIsLoading } = useLoading(); const { userInfo } = useUserStore(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); @@ -481,7 +473,7 @@ const Chat = ({ navigator.vibrate?.(50); // 震动 50 毫秒 - if (!isPcDevice) { + if (!isPc) { PhoneContextShow.current = true; } @@ -493,7 +485,7 @@ const Chat = ({ return false; }, - [isPcDevice] + [isPc] ); // 获取对话信息 @@ -636,61 +628,9 @@ const Chat = ({ > {/* pc always show history. */} {(isPc || !modelId) && ( - div': { visibility: 'visible', opacity: 1 } - }} - > - setFoldSlideBar(!foldSliderBar)} - > - - - - - - + + + )} {/* 聊天内容 */} @@ -906,7 +846,7 @@ const Chat = ({ }} onKeyDown={(e) => { // 触发快捷发送 - if (isPcDevice && e.keyCode === 13 && !e.shiftKey) { + if (isPc && e.keyCode === 13 && !e.shiftKey) { sendPrompt(); e.preventDefault(); } @@ -1008,8 +948,7 @@ const Chat = ({ Chat.getInitialProps = ({ query, req }: any) => { return { modelId: query?.modelId || '', - chatId: query?.chatId || '', - isPcDevice: !/Mobile/.test(req?.headers?.['user-agent']) + chatId: query?.chatId || '' }; }; diff --git a/src/pages/chat/share.tsx b/src/pages/chat/share.tsx index 23be36d27..84b39b705 100644 --- a/src/pages/chat/share.tsx +++ b/src/pages/chat/share.tsx @@ -31,7 +31,7 @@ import { ModalHeader } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; -import { useScreen } from '@/hooks/useScreen'; +import { useGlobalStore } from '@/store/global'; import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools'; @@ -47,6 +47,7 @@ import { htmlTemplate } from '@/constants/common'; import { useUserStore } from '@/store/user'; import Loading from '@/components/Loading'; import Markdown from '@/components/Markdown'; +import SideBar from '@/components/SideBar'; import Empty from './components/Empty'; const ShareHistory = dynamic(() => import('./components/ShareHistory'), { @@ -58,15 +59,7 @@ import styles from './index.module.scss'; const textareaMinH = '22px'; -const Chat = ({ - shareId, - historyId, - isPcDevice -}: { - shareId: string; - historyId: string; - isPcDevice: boolean; -}) => { +const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) => { const router = useRouter(); const theme = useTheme(); @@ -87,7 +80,6 @@ const Chat = ({ top: number; message: ChatSiteItemType; }>(); - const [foldSliderBar, setFoldSlideBar] = useState(false); const { password, @@ -108,7 +100,7 @@ const Chat = ({ const { toast } = useToast(); const { copyData } = useCopyData(); - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { isPc } = useGlobalStore(); const { Loading, setIsLoading } = useLoading(); const { userInfo } = useUserStore(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); @@ -426,7 +418,7 @@ const Chat = ({ navigator.vibrate?.(50); // 震动 50 毫秒 - if (!isPcDevice) { + if (!isPc) { PhoneContextShow.current = true; } @@ -438,7 +430,7 @@ const Chat = ({ return false; }, - [isPcDevice] + [isPc] ); // 获取对话信息 @@ -543,57 +535,13 @@ const Chat = ({ > {/* pc always show history. */} {isPc && ( - div': { visibility: 'visible', opacity: 1 } - }} - > - setFoldSlideBar(!foldSliderBar)} - > - - - - - - + + + )} {/* 聊天内容 */} @@ -623,13 +571,7 @@ const Chat = ({ onClick={onOpenSlider} /> )} - + {shareChatData.model.name} {shareChatData.history.length > 0 ? ` (${shareChatData.history.length})` : ''} @@ -797,7 +739,7 @@ const Chat = ({ }} onKeyDown={(e) => { // 触发快捷发送 - if (isPcDevice && e.keyCode === 13 && !e.shiftKey) { + if (isPc && e.keyCode === 13 && !e.shiftKey) { sendPrompt(); e.preventDefault(); } @@ -854,7 +796,6 @@ const Chat = ({ onclickDelHistory={delShareHistoryById} onclickExportChat={onclickExportChat} onCloseSlider={onCloseSlider} - isPcDevice={isPcDevice} /> @@ -924,8 +865,7 @@ const Chat = ({ Chat.getInitialProps = ({ query, req }: any) => { return { shareId: query?.shareId || '', - historyId: query?.historyId || '', - isPcDevice: !/Mobile/.test(req?.headers?.['user-agent']) + historyId: query?.historyId || '' }; }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5fe013798..1ef4d54c2 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,8 +4,8 @@ import Markdown from '@/components/Markdown'; import { useMarkdown } from '@/hooks/useMarkdown'; import { getFilling } from '@/api/system'; import { useQuery } from '@tanstack/react-query'; -import { useScreen } from '@/hooks/useScreen'; import { useRouter } from 'next/router'; +import { useGlobalStore } from '@/store/global'; import styles from './index.module.scss'; @@ -13,7 +13,7 @@ const Home = () => { const router = useRouter(); const { inviterId } = router.query as { inviterId: string }; const { data } = useMarkdown({ url: '/intro.md' }); - const { isPc } = useScreen(); + const { isPc } = useGlobalStore(); useEffect(() => { if (inviterId) { diff --git a/src/pages/model/components/detail/components/ModelDataCard.tsx b/src/pages/kb/components/DataCard.tsx similarity index 61% rename from src/pages/model/components/detail/components/ModelDataCard.tsx rename to src/pages/kb/components/DataCard.tsx index 79a6959e6..088ef4a19 100644 --- a/src/pages/model/components/detail/components/ModelDataCard.tsx +++ b/src/pages/kb/components/DataCard.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useRef, useEffect } from 'react'; +import React, { useCallback, useState, useRef } from 'react'; import { Box, TableContainer, @@ -21,30 +21,29 @@ import { } from '@chakra-ui/react'; import { QuestionOutlineIcon } from '@chakra-ui/icons'; import type { BoxProps } from '@chakra-ui/react'; -import type { ModelDataItemType } from '@/types/model'; +import type { KbDataItemType } from '@/types/plugin'; import { ModelDataStatusMap } from '@/constants/model'; import { usePagination } from '@/hooks/usePagination'; import { - getModelDataList, - delOneModelData, - getModelSplitDataListLen, - getExportDataList -} from '@/api/model'; + getKbDataList, + getExportDataList, + delOneKbDataByDataId, + getTrainingData +} from '@/api/plugins/kb'; import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons'; import { useLoading } from '@/hooks/useLoading'; import { fileDownload } from '@/utils/file'; -import dynamic from 'next/dynamic'; import { useMutation, useQuery } from '@tanstack/react-query'; +import { useToast } from '@/hooks/useToast'; import Papa from 'papaparse'; +import dynamic from 'next/dynamic'; import InputModal, { FormData as InputDataType } from './InputDataModal'; const SelectFileModal = dynamic(() => import('./SelectFileModal')); const SelectCsvModal = dynamic(() => import('./SelectCsvModal')); -const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => { - const { Loading, setIsLoading } = useLoading(); +const DataCard = ({ kbId }: { kbId: string }) => { const lastSearch = useRef(''); - const [searchText, setSearchText] = useState(''); const tdStyles = useRef({ fontSize: 'xs', minW: '150px', @@ -53,6 +52,9 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean whiteSpace: 'pre-wrap', overflowY: 'auto' }); + const [searchText, setSearchText] = useState(''); + const { Loading, setIsLoading } = useLoading(); + const { toast } = useToast(); const { data: modelDataList, @@ -61,19 +63,20 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean total, getData, pageNum - } = usePagination({ - api: getModelDataList, + } = usePagination({ + api: getKbDataList, pageSize: 10, params: { - modelId, + kbId, searchText }, defaultRequest: false }); - useEffect(() => { + useQuery(['getKbData', kbId], () => { getData(1); - }, [modelId, getData]); + return null; + }); const [editInputData, setEditInputData] = useState(); @@ -90,7 +93,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean const { data: { splitDataQueue = 0, embeddingQueue = 0 } = {}, refetch } = useQuery( ['getModelSplitDataList'], - () => getModelSplitDataListLen(modelId), + () => getTrainingData(kbId), { onError(err) { console.log(err); @@ -107,14 +110,15 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean [getData, refetch] ); + // interval get data useQuery(['refetchData'], () => refetchData(pageNum), { refetchInterval: 5000, enabled: splitDataQueue > 0 || embeddingQueue > 0 }); - // 获取所有的数据,并导出 json + // get al data and export csv const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({ - mutationFn: () => getExportDataList(modelId), + mutationFn: () => getExportDataList(kbId), onSuccess(res) { try { setIsLoading(true); @@ -132,61 +136,61 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean } setIsLoading(false); }, - onError(err) { + onError(err: any) { + toast({ + title: typeof err === 'string' ? err : err?.message || '导出异常', + status: 'error' + }); console.log(err); } }); return ( - + 知识库数据: {total}组 - {isOwner && ( - <> - } - aria-label={'refresh'} - variant={'outline'} - mr={4} - size={'sm'} - onClick={() => refetchData(pageNum)} - /> - + + + 导入 + + + + setEditInputData({ + a: '', + q: '' + }) + } > - 导出 - - - - 导入 - - - - setEditInputData({ - a: '', - q: '' - }) - } - > - 手动输入 - - 文本/文件拆分 - csv 问答对导入 - - - - )} + 手动输入 + + 文本/文件拆分 + csv 问答对导入 + + - {isOwner && (splitDataQueue > 0 || embeddingQueue > 0) && ( + {(splitDataQueue > 0 || embeddingQueue > 0) && ( {splitDataQueue > 0 ? `${splitDataQueue}条数据正在拆分,` : ''} {embeddingQueue > 0 ? `${embeddingQueue}条数据正在生成索引,` : ''} @@ -216,7 +220,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean - + @@ -232,7 +236,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean - {isOwner && } + @@ -245,35 +249,33 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean {item.a || '-'} - {isOwner && ( - - )} + ))} @@ -287,24 +289,20 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean {editInputData !== undefined && ( setEditInputData(undefined)} onSuccess={refetchData} /> )} {isOpenSelectFileModal && ( - + )} {isOpenSelectCsvModal && ( - + )} ); }; -export default ModelDataCard; +export default DataCard; diff --git a/src/pages/kb/components/Detail.tsx b/src/pages/kb/components/Detail.tsx new file mode 100644 index 000000000..4e4c7df3d --- /dev/null +++ b/src/pages/kb/components/Detail.tsx @@ -0,0 +1,245 @@ +import React, { useCallback, useState, useRef } from 'react'; +import { useRouter } from 'next/router'; +import { + Card, + Box, + Flex, + Button, + Tooltip, + Image, + FormControl, + Input, + Tag, + IconButton +} from '@chakra-ui/react'; +import { QuestionOutlineIcon, DeleteIcon } from '@chakra-ui/icons'; +import { useToast } from '@/hooks/useToast'; +import { useForm } from 'react-hook-form'; +import { useQuery } from '@tanstack/react-query'; +import { useUserStore } from '@/store/user'; +import { delKbById, putKbById } from '@/api/plugins/kb'; +import { useLoading } from '@/hooks/useLoading'; +import { KbItemType } from '@/types/plugin'; +import { useSelectFile } from '@/hooks/useSelectFile'; +import { useConfirm } from '@/hooks/useConfirm'; +import { compressImg } from '@/utils/file'; +import DataCard from './DataCard'; + +const Detail = ({ kbId }: { kbId: string }) => { + const { toast } = useToast(); + const router = useRouter(); + const InputRef = useRef(null); + const { setLastKbId, KbDetail, getKbDetail, loadKbList, myKbList } = useUserStore(); + const { Loading, setIsLoading } = useLoading(); + const [btnLoading, setBtnLoading] = useState(false); + const [refresh, setRefresh] = useState(false); + + const { getValues, formState, setValue, reset, register, handleSubmit } = useForm({ + defaultValues: KbDetail + }); + const { openConfirm, ConfirmChild } = useConfirm({ + content: '确认删除该知识库?数据将无法恢复,请确认!' + }); + + const { File, onOpen: onOpenSelectFile } = useSelectFile({ + fileType: '.jpg,.png', + multiple: false + }); + + const { isLoading } = useQuery([kbId, myKbList], () => getKbDetail(kbId), { + onSuccess(res) { + kbId && setLastKbId(kbId); + if (res) { + reset(res); + if (InputRef.current) { + InputRef.current.value = res.tags; + } + } + }, + onError(err: any) { + toast({ + title: err?.message || '获取AI助手异常', + status: 'error' + }); + setLastKbId(''); + router.replace('/model'); + } + }); + + /* 点击删除 */ + const onclickDelKb = useCallback(async () => { + setIsLoading(true); + try { + await delKbById(kbId); + toast({ + title: '删除成功', + status: 'success' + }); + router.replace(`/kb?kbId=${myKbList.find((item) => item._id !== kbId)?._id || ''}`); + await loadKbList(true); + } catch (err: any) { + toast({ + title: err?.message || '删除失败', + status: 'error' + }); + } + setIsLoading(false); + }, [setIsLoading, kbId, toast, router, myKbList, loadKbList]); + + const saveSubmitSuccess = useCallback( + async (data: KbItemType) => { + setBtnLoading(true); + try { + await putKbById({ + id: kbId, + ...data + }); + toast({ + title: '更新成功', + status: 'success' + }); + loadKbList(true); + } catch (err: any) { + toast({ + title: err?.message || '更新失败', + status: 'error' + }); + } + setBtnLoading(false); + }, + [kbId, loadKbList, toast] + ); + const saveSubmitError = useCallback(() => { + // deep search message + const deepSearch = (obj: any): string => { + if (!obj) return '提交表单错误'; + if (!!obj.message) { + return obj.message; + } + return deepSearch(Object.values(obj)[0]); + }; + toast({ + title: deepSearch(formState.errors), + status: 'error', + duration: 4000, + isClosable: true + }); + }, [formState.errors, toast]); + + const onSelectFile = useCallback( + async (e: File[]) => { + const file = e[0]; + if (!file) return; + try { + const base64 = await compressImg({ + file, + maxW: 100, + maxH: 100 + }); + setValue('avatar', base64); + loadKbList(true); + } catch (err: any) { + toast({ + title: typeof err === 'string' ? err : '头像选择异常', + status: 'warning' + }); + } + }, + [loadKbList, setValue, toast] + ); + + return ( + + + + + 知识库信息 + + {KbDetail._id && ( + <> + + } + aria-label={''} + variant={'solid'} + colorScheme={'red'} + onClick={openConfirm(onclickDelKb)} + /> + + )} + + + + 头像 + + + + + + + 名称 + + + + + + + + 标签 + + + + + { + setValue('tags', e.target.value); + setRefresh(!refresh); + }} + /> + + {getValues('tags') + .split(' ') + .filter((item) => item) + .map((item, i) => ( + + {item} + + ))} + + + + + + + + + + + + ); +}; + +export default Detail; diff --git a/src/pages/model/components/detail/components/InputDataModal.tsx b/src/pages/kb/components/InputDataModal.tsx similarity index 88% rename from src/pages/model/components/detail/components/InputDataModal.tsx rename to src/pages/kb/components/InputDataModal.tsx index 912135a02..2c7195a38 100644 --- a/src/pages/model/components/detail/components/InputDataModal.tsx +++ b/src/pages/kb/components/InputDataModal.tsx @@ -11,17 +11,15 @@ import { Textarea } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; -import { postModelDataInput, putModelDataById } from '@/api/model'; +import { postKbDataFromList, putKbDataById } from '@/api/plugins/kb'; import { useToast } from '@/hooks/useToast'; -import { customAlphabet } from 'nanoid'; -const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12); export type FormData = { dataId?: string; a: string; q: string }; const InputDataModal = ({ onClose, onSuccess, - modelId, + kbId, defaultValues = { a: '', q: '' @@ -29,7 +27,7 @@ const InputDataModal = ({ }: { onClose: () => void; onSuccess: () => void; - modelId: string; + kbId: string; defaultValues?: FormData; }) => { const [importing, setImporting] = useState(false); @@ -54,8 +52,8 @@ const InputDataModal = ({ setImporting(true); try { - const res = await postModelDataInput({ - modelId: modelId, + const res = await postKbDataFromList({ + kbId, data: [ { a: e.a, @@ -65,8 +63,8 @@ const InputDataModal = ({ }); toast({ - title: res === 0 ? '导入数据成功,需要一段时间训练' : '数据导入异常', - status: res === 0 ? 'success' : 'warning' + title: res === 0 ? '可能已存在完全一致的数据' : '导入数据成功,需要一段时间训练', + status: 'success' }); reset({ a: '', @@ -82,7 +80,7 @@ const InputDataModal = ({ } setImporting(false); }, - [modelId, onSuccess, reset, toast] + [kbId, onSuccess, reset, toast] ); const updateData = useCallback( @@ -90,7 +88,7 @@ const InputDataModal = ({ if (!e.dataId) return; if (e.a !== defaultValues.a || e.q !== defaultValues.q) { - await putModelDataById({ + await putKbDataById({ dataId: e.dataId, a: e.a, q: e.q === defaultValues.q ? '' : e.q diff --git a/src/pages/kb/components/KbList.tsx b/src/pages/kb/components/KbList.tsx new file mode 100644 index 000000000..d23271242 --- /dev/null +++ b/src/pages/kb/components/KbList.tsx @@ -0,0 +1,150 @@ +import React, { useCallback, useState } from 'react'; +import { Box, Flex, useTheme, Input, IconButton, Tooltip, Image, Tag } from '@chakra-ui/react'; +import { AddIcon } from '@chakra-ui/icons'; +import { useRouter } from 'next/router'; +import { postCreateKb } from '@/api/plugins/kb'; +import { useLoading } from '@/hooks/useLoading'; +import { useToast } from '@/hooks/useToast'; +import { useQuery } from '@tanstack/react-query'; +import { useUserStore } from '@/store/user'; +import MyIcon from '@/components/Icon'; + +const KbList = ({ kbId }: { kbId: string }) => { + const theme = useTheme(); + const router = useRouter(); + const { toast } = useToast(); + const { Loading, setIsLoading } = useLoading(); + const { myKbList, loadKbList } = useUserStore(); + const [searchText, setSearchText] = useState(''); + + /* 加载模型 */ + const { isLoading } = useQuery(['loadModels'], () => loadKbList(false)); + + const handleCreateModel = useCallback(async () => { + setIsLoading(true); + try { + const name = `知识库${myKbList.length + 1}`; + const id = await postCreateKb({ name }); + await loadKbList(true); + toast({ + title: '创建成功', + status: 'success' + }); + router.replace(`/kb?kbId=${id}`); + } catch (err: any) { + toast({ + title: typeof err === 'string' ? err : err.message || '出现了意外', + status: 'error' + }); + } + setIsLoading(false); + }, [loadKbList, myKbList.length, router, setIsLoading, toast]); + + return ( + + + + setSearchText(e.target.value)} + /> + {searchText && ( + setSearchText('')} + /> + )} + + + } + aria-label={''} + variant={'outline'} + onClick={handleCreateModel} + /> + + + + {myKbList.map((item) => ( + { + if (item._id === kbId) return; + router.push(`/kb?kbId=${item._id}`); + }} + > + + + + {item.name} + + {/* tags */} + + {!item.tags ? ( + <>{item.tags || '你还没设置标签~'} + ) : ( + item.tags.split(' ').map((item, i) => ( + + {item} + + )) + )} + + + + ))} + + {!isLoading && myKbList.length === 0 && ( + + + + 知识库空空如也~ + + + )} + + + + ); +}; + +export default KbList; diff --git a/src/pages/model/components/detail/components/SelectCsvModal.tsx b/src/pages/kb/components/SelectCsvModal.tsx similarity index 89% rename from src/pages/model/components/detail/components/SelectCsvModal.tsx rename to src/pages/kb/components/SelectCsvModal.tsx index ef8322c06..54f7f8376 100644 --- a/src/pages/model/components/detail/components/SelectCsvModal.tsx +++ b/src/pages/kb/components/SelectCsvModal.tsx @@ -15,7 +15,7 @@ import { useSelectFile } from '@/hooks/useSelectFile'; import { useConfirm } from '@/hooks/useConfirm'; import { readCsvContent } from '@/utils/file'; import { useMutation } from '@tanstack/react-query'; -import { postModelDataCsvData } from '@/api/model'; +import { postKbDataFromList } from '@/api/plugins/kb'; import Markdown from '@/components/Markdown'; import { useMarkdown } from '@/hooks/useMarkdown'; import { fileDownload } from '@/utils/file'; @@ -25,16 +25,16 @@ const csvTemplate = `question,answer\n"什么是 laf","laf 是一个云函数开 const SelectJsonModal = ({ onClose, onSuccess, - modelId + kbId }: { onClose: () => void; onSuccess: () => void; - modelId: string; + kbId: string; }) => { const [selecting, setSelecting] = useState(false); const { toast } = useToast(); const { File, onOpen } = useSelectFile({ fileType: '.csv', multiple: false }); - const [fileData, setFileData] = useState([]); + const [fileData, setFileData] = useState<{ q: string; a: string }[]>([]); const { openConfirm, ConfirmChild } = useConfirm({ content: '确认导入该数据集?' }); @@ -48,7 +48,12 @@ const SelectJsonModal = ({ if (header[0] !== 'question' || header[1] !== 'answer') { throw new Error('csv 文件格式有误'); } - setFileData(data); + setFileData( + data.map((item) => ({ + q: item[0] || '', + a: item[1] || '' + })) + ); } catch (error: any) { console.log(error); toast({ @@ -63,8 +68,13 @@ const SelectJsonModal = ({ const { mutate, isLoading } = useMutation({ mutationFn: async () => { - if (!fileData) return; - const res = await postModelDataCsvData(modelId, fileData); + if (!fileData || fileData.length === 0) return; + + const res = await postKbDataFromList({ + kbId, + data: fileData + }); + toast({ title: `导入数据成功,最终导入: ${res || 0} 条数据。需要一段时间训练`, status: 'success', @@ -120,10 +130,10 @@ const SelectJsonModal = ({ {fileData.map((item, index) => ( - Q{index + 1}. {item[0]} + Q{index + 1}. {item.q} - A{index + 1}. {item[1]} + A{index + 1}. {item.a} ))} diff --git a/src/pages/model/components/detail/components/SelectFileModal.tsx b/src/pages/kb/components/SelectFileModal.tsx similarity index 96% rename from src/pages/model/components/detail/components/SelectFileModal.tsx rename to src/pages/kb/components/SelectFileModal.tsx index 588c691ec..bde43edcf 100644 --- a/src/pages/model/components/detail/components/SelectFileModal.tsx +++ b/src/pages/kb/components/SelectFileModal.tsx @@ -17,10 +17,10 @@ import { useSelectFile } from '@/hooks/useSelectFile'; import { useConfirm } from '@/hooks/useConfirm'; import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file'; import { useMutation } from '@tanstack/react-query'; -import { postModelDataSplitData } from '@/api/model'; -import { formatPrice } from '@/utils/user'; +import { postSplitData } from '@/api/plugins/kb'; import Radio from '@/components/Radio'; import { splitText_token } from '@/utils/file'; +import { SplitTextTypEnum } from '@/constants/plugin'; const fileExtension = '.txt,.doc,.docx,.pdf,.md'; @@ -42,17 +42,17 @@ const modeMap = { const SelectFileModal = ({ onClose, onSuccess, - modelId + kbId }: { onClose: () => void; onSuccess: () => void; - modelId: string; + kbId: string; }) => { const [btnLoading, setBtnLoading] = useState(false); const { toast } = useToast(); const [prompt, setPrompt] = useState(''); const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true }); - const [mode, setMode] = useState<'qa' | 'subsection'>('qa'); + const [mode, setMode] = useState<`${SplitTextTypEnum}`>(SplitTextTypEnum.subsection); const [fileTextArr, setFileTextArr] = useState(['']); const [splitRes, setSplitRes] = useState<{ tokens: number; chunks: string[] }>({ tokens: 0, @@ -107,8 +107,8 @@ const SelectFileModal = ({ mutationFn: async () => { if (splitRes.chunks.length === 0) return; - await postModelDataSplitData({ - modelId, + await postSplitData({ + kbId, chunks: splitRes.chunks, prompt: `下面是"${prompt || '一段长文本'}"`, mode diff --git a/src/pages/kb/index.tsx b/src/pages/kb/index.tsx new file mode 100644 index 000000000..ffdaf7f1f --- /dev/null +++ b/src/pages/kb/index.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import { useGlobalStore } from '@/store/global'; +import { useRouter } from 'next/router'; +import { useUserStore } from '@/store/user'; +import SideBar from '@/components/SideBar'; +import KbList from './components/KbList'; +import KbDetail from './components/Detail'; + +const Kb = ({ kbId }: { kbId: string }) => { + const router = useRouter(); + const { isPc } = useGlobalStore(); + const { lastKbId } = useUserStore(); + + // redirect + useEffect(() => { + if (isPc && !kbId && lastKbId) { + router.replace(`/kb?kbId=${lastKbId}`); + } + }, [isPc, kbId, lastKbId, router]); + + return ( + + {/* 模型列表 */} + {(isPc || !kbId) && ( + + + + )} + + {kbId && } + + + ); +}; + +export default Kb; + +Kb.getInitialProps = ({ query, req }: any) => { + return { + kbId: query?.kbId || '' + }; +}; diff --git a/src/pages/login/components/ForgetPasswordForm.tsx b/src/pages/login/components/ForgetPasswordForm.tsx index ea5a7076a..03098bb35 100644 --- a/src/pages/login/components/ForgetPasswordForm.tsx +++ b/src/pages/login/components/ForgetPasswordForm.tsx @@ -5,7 +5,6 @@ import { PageTypeEnum } from '../../../constants/user'; import { postFindPassword } from '@/api/user'; import { useSendCode } from '@/hooks/useSendCode'; import type { ResLogin } from '@/api/response/user'; -import { useScreen } from '@/hooks/useScreen'; import { useToast } from '@/hooks/useToast'; interface Props { @@ -22,7 +21,6 @@ interface RegisterType { const RegisterForm = ({ setPageType, loginSuccess }: Props) => { const { toast } = useToast(); - const { mediaLgMd } = useScreen(); const { register, handleSubmit, @@ -81,7 +79,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { { { ml={5} w={'145px'} maxW={'50%'} - size={mediaLgMd} + size={['md', 'lg']} onClick={onclickSendCode} isDisabled={codeCountDown > 0} isLoading={codeSending} @@ -125,7 +123,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { { (getValues('password') === val ? true : '两次密码不一致') })} @@ -170,7 +168,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { type="submit" mt={5} w={'100%'} - size={mediaLgMd} + size={['md', 'lg']} colorScheme="blue" isLoading={requesting} > diff --git a/src/pages/login/components/LoginForm.tsx b/src/pages/login/components/LoginForm.tsx index 8263c1af3..61f21d990 100644 --- a/src/pages/login/components/LoginForm.tsx +++ b/src/pages/login/components/LoginForm.tsx @@ -5,7 +5,6 @@ import { PageTypeEnum } from '@/constants/user'; import { postLogin } from '@/api/user'; import type { ResLogin } from '@/api/response/user'; import { useToast } from '@/hooks/useToast'; -import { useScreen } from '@/hooks/useScreen'; interface Props { setPageType: Dispatch<`${PageTypeEnum}`>; @@ -19,7 +18,6 @@ interface LoginFormType { const LoginForm = ({ setPageType, loginSuccess }: Props) => { const { toast } = useToast(); - const { mediaLgMd } = useScreen(); const { register, handleSubmit, @@ -62,7 +60,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { { { type="submit" mt={8} w={'100%'} - size={mediaLgMd} + size={['md', 'lg']} colorScheme="blue" isLoading={requesting} > diff --git a/src/pages/login/components/RegisterForm.tsx b/src/pages/login/components/RegisterForm.tsx index 719d97f74..87cf12411 100644 --- a/src/pages/login/components/RegisterForm.tsx +++ b/src/pages/login/components/RegisterForm.tsx @@ -5,7 +5,6 @@ import { PageTypeEnum } from '@/constants/user'; import { postRegister } from '@/api/user'; import { useSendCode } from '@/hooks/useSendCode'; import type { ResLogin } from '@/api/response/user'; -import { useScreen } from '@/hooks/useScreen'; import { useToast } from '@/hooks/useToast'; import { useRouter } from 'next/router'; import { postCreateModel } from '@/api/model'; @@ -25,7 +24,6 @@ interface RegisterType { const RegisterForm = ({ setPageType, loginSuccess }: Props) => { const { inviterId = '' } = useRouter().query as { inviterId: string }; const { toast } = useToast(); - const { mediaLgMd } = useScreen(); const { register, handleSubmit, @@ -89,7 +87,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { { { ml={5} w={'145px'} maxW={'50%'} - size={mediaLgMd} + size={['md', 'lg']} onClick={onclickSendCode} isDisabled={codeCountDown > 0} isLoading={codeSending} @@ -133,7 +131,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { { (getValues('password') === val ? true : '两次密码不一致') })} @@ -178,7 +176,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { type="submit" mt={5} w={'100%'} - size={mediaLgMd} + size={['md', 'lg']} colorScheme="blue" isLoading={requesting} > diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 3de87e2d6..16af48ea8 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react'; import styles from './index.module.scss'; import { Box, Flex, Image } from '@chakra-ui/react'; import { PageTypeEnum } from '@/constants/user'; -import { useScreen } from '@/hooks/useScreen'; +import { useGlobalStore } from '@/store/global'; import type { ResLogin } from '@/api/response/user'; import { useRouter } from 'next/router'; import { useUserStore } from '@/store/user'; @@ -12,10 +12,10 @@ import dynamic from 'next/dynamic'; const RegisterForm = dynamic(() => import('./components/RegisterForm')); const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm')); -const Login = ({ isPcDevice }: { isPcDevice: boolean }) => { +const Login = () => { const router = useRouter(); const { lastRoute = '' } = router.query as { lastRoute: string }; - const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { isPc } = useGlobalStore(); const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login); const { setUserInfo, setLastModelId, loadMyModels } = useUserStore(); const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore(); @@ -114,9 +114,3 @@ const Login = ({ isPcDevice }: { isPcDevice: boolean }) => { }; export default Login; - -Login.getInitialProps = ({ query, req }: any) => { - return { - isPcDevice: !/Mobile/.test(req?.headers?.['user-agent']) - }; -}; diff --git a/src/pages/model/components/ModelList.tsx b/src/pages/model/components/ModelList.tsx index 7ba514ff5..f8618d5b3 100644 --- a/src/pages/model/components/ModelList.tsx +++ b/src/pages/model/components/ModelList.tsx @@ -126,7 +126,7 @@ const ModelList = ({ modelId }: { modelId: string }) => { {...(modelId === item._id ? { backgroundColor: '#eff0f1', - borderLeftColor: 'myBlue.600' + borderLeftColor: 'myBlue.600 !important' } : {})} onClick={() => { diff --git a/src/pages/model/components/detail/components/SelectUrlModal.tsx b/src/pages/model/components/detail/components/SelectUrlModal.tsx deleted file mode 100644 index 3b9e526c7..000000000 --- a/src/pages/model/components/detail/components/SelectUrlModal.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, - Flex, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalCloseButton, - ModalBody, - Input, - Textarea -} from '@chakra-ui/react'; -import { useToast } from '@/hooks/useToast'; -import { useConfirm } from '@/hooks/useConfirm'; -import { useMutation } from '@tanstack/react-query'; -import { postModelDataSplitData, getWebContent } from '@/api/model'; -import { formatPrice } from '@/utils/user'; - -const SelectUrlModal = ({ - onClose, - onSuccess, - modelId -}: { - onClose: () => void; - onSuccess: () => void; - modelId: string; -}) => { - const { toast } = useToast(); - const [webUrl, setWebUrl] = useState(''); - const [webText, setWebText] = useState(''); - const [prompt, setPrompt] = useState(''); // 提示词 - const { openConfirm, ConfirmChild } = useConfirm({ - content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,任务讲被终止。' - }); - - const { mutate: onclickImport, isLoading: isImporting } = useMutation({ - mutationFn: async () => { - if (!webText) return; - await postModelDataSplitData({ - modelId, - chunks: [], - prompt: `下面是"${prompt || '一段长文本'}"`, - mode: 'qa' - }); - toast({ - title: '导入数据成功,需要一段拆解和训练', - status: 'success' - }); - onClose(); - onSuccess(); - }, - onError(error) { - console.log(error); - toast({ - title: '导入数据失败', - status: 'error' - }); - } - }); - - const { mutate: onclickFetchingUrl, isLoading: isFetching } = useMutation({ - mutationFn: async () => { - if (!webUrl) return; - const res = await getWebContent(webUrl); - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(res, 'text/html'); - const data = htmlDoc?.body?.innerText || ''; - - if (!data) { - throw new Error('获取不到数据'); - } - setWebText(data.replace(/\s+/g, ' ')); - }, - onError(error) { - console.log(error); - toast({ - status: 'error', - title: '获取网站内容失败' - }); - } - }); - - return ( - - - - 静态网站内容导入 - - - - - 根据网站地址,获取网站文本内容(请注意仅能获取静态网站文本,注意看下获取后的内容是否正确)。Gpt会对文本进行 - QA 拆分,需要较长训练时间,拆分需要消耗 tokens,账号余额不足时,未拆分的数据会被删除。 - - - 网站地址 - setWebUrl(e.target.value)} - size={'sm'} - /> - - - - - 下面是 - - setPrompt(e.target.value)} - size={'sm'} - /> - -
补充知识 状态操作操作
{ModelDataStatusMap[item.status]} - } - variant={'outline'} - aria-label={'delete'} - size={'sm'} - onClick={() => - setEditInputData({ - dataId: item.id, - q: item.q, - a: item.a - }) - } - /> - } - variant={'outline'} - colorScheme={'gray'} - aria-label={'delete'} - size={'sm'} - onClick={async () => { - await delOneModelData(item.id); - refetchData(pageNum); - }} - /> - + } + variant={'outline'} + aria-label={'delete'} + size={'sm'} + onClick={() => + setEditInputData({ + dataId: item.id, + q: item.q, + a: item.a + }) + } + /> + } + variant={'outline'} + colorScheme={'gray'} + aria-label={'delete'} + size={'sm'} + onClick={async () => { + await delOneKbDataByDataId(item.id); + refetchData(pageNum); + }} + /> +