diff --git a/README.md b/README.md index 55f7a51..467c938 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - ## Devlop Mode + ### Getting Started First, Config Server Url: next.config.js -```javascript - const nextConfig = { - output: 'export', - experimental: { - esmExternals: 'loose' - }, - typescript: { - ignoreBuildErrors: true - }, - env: { - API_BASE_URL: process.env.API_BASE_URL || your server url - }, - trailingSlash: true - } + +```js +const nextConfig = { + output: 'export', + experimental: { + esmExternals: 'loose', + }, + typescript: { + ignoreBuildErrors: true, + }, + env: { + API_BASE_URL: process.env.API_BASE_URL || 'your server url', + }, + trailingSlash: true, +}; ``` Second, run the development server: @@ -39,6 +40,7 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More + To learn more about Next.js, take a look at the following resources: Next.js Documentation - learn about Next.js features and API. @@ -46,12 +48,15 @@ Learn Next.js - an interactive Next.js tutorial. You can check out the Next.js GitHub repository - your feedback and contributions are welcome! ## Deploy on Vercel + The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js. Check out our Next.js deployment documentation for more details. ## Product Mode + ### Use In DB-GPT + ```bash npm run compile diff --git a/app/chat-context.tsx b/app/chat-context.tsx index 0f342da..0436ff9 100644 --- a/app/chat-context.tsx +++ b/app/chat-context.tsx @@ -2,18 +2,21 @@ import { createContext, useEffect, useMemo, useState } from 'react'; import { apiInterceptors, getDialogueList, getUsableModels } from '@/client/api'; import { useRequest } from 'ahooks'; import { useRouter } from 'next/router'; -import { DialogueListResponse } from '@/types/chart'; +import { DialogueListResponse, IChatDialogueSchema } from '@/types/chat'; +import { useSearchParams } from 'next/navigation'; interface IChatContext { isContract?: boolean; isMenuExpand?: boolean; - scene: string; + scene: IChatDialogueSchema['chat_mode'] | (string & {}); chatId: string; model: string; dbParam?: string; modelList: Array; - setModel: (val: string) => void; + agentList: string[]; dialogueList?: DialogueListResponse; + setAgentList?: (val: string[]) => void; + setModel: (val: string) => void; setIsContract: (val: boolean) => void; setIsMenuExpand: (val: boolean) => void; setDbParam: (val: string) => void; @@ -28,8 +31,10 @@ const ChatContext = createContext({ modelList: [], model: '', dbParam: undefined, - setModel: () => {}, dialogueList: [], + agentList: [], + setAgentList: () => {}, + setModel: () => {}, setIsContract: () => {}, setIsMenuExpand: () => {}, setDbParam: () => void 0, @@ -38,11 +43,15 @@ const ChatContext = createContext({ }); const ChatContextProvider = ({ children }: { children: React.ReactElement }) => { - const { query: { id = '', scene = '' } = {} } = useRouter(); + const searchParams = useSearchParams(); + const chatId = searchParams?.get('id') ?? ''; + const scene = searchParams?.get('scene') ?? ''; + const db_param = searchParams?.get('db_param') ?? ''; const [isContract, setIsContract] = useState(false); const [model, setModel] = useState(''); const [isMenuExpand, setIsMenuExpand] = useState(scene !== 'chat_dashboard'); - const [dbParam, setDbParam] = useState(); + const [dbParam, setDbParam] = useState(db_param); + const [agentList, setAgentList] = useState([]); const { run: queryDialogueList, @@ -66,17 +75,19 @@ const ChatContextProvider = ({ children }: { children: React.ReactElement }) => setModel(modelList[0]); }, [modelList, modelList?.length]); - const currentDialogue = useMemo(() => dialogueList.find((item: any) => item.conv_uid === id), [id, dialogueList]); + const currentDialogue = useMemo(() => dialogueList.find((item: any) => item.conv_uid === chatId), [chatId, dialogueList]); const contextValue = { isContract, isMenuExpand, - scene: scene as string, - chatId: id as string, + scene, + chatId, modelList, model, - dbParam, - setModel, + dbParam: dbParam || db_param, dialogueList, + agentList, + setAgentList, + setModel, setIsContract, setIsMenuExpand, setDbParam, diff --git a/app/i18n.ts b/app/i18n.ts index 8406af7..19d2d0a 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -1,4 +1,3 @@ -import { STORAGE_LANG_KEY } from '@/constant'; import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; @@ -9,7 +8,8 @@ const resources = { space: 'space', Vector: 'Vector', Owner: 'Owner', - Docs: 'Docs', + Count: 'Count', + File_type_Invalid: 'The file type is invalid', Knowledge_Space_Config: 'Knowledge Space Config', Choose_a_Datasource_type: 'Choose a Datasource type', Setup_the_Datasource: 'Setup the Datasource', @@ -95,8 +95,12 @@ const resources = { create_model: 'Create Model', model_select_tips: 'Please select a model', submit: 'Submit', + close: 'Close', start_model_success: 'Start model success', download_model_tip: 'Please download model first.', + Plugins: 'Plugins', + try_again: 'Try again', + no_data: 'No data', Open_Sidebar: 'Unfold', cancel: 'Cancel', Edit_Success: 'Edit Success', @@ -134,6 +138,17 @@ const resources = { Copry_error: 'Copy failed', Click_Select: 'Click&Select', Quick_Start: 'Quick Start', + Select_Plugins: 'Select Plugins', + Search: 'Search', + Update_From_Github: 'Upload From Github', + Reset: 'Reset', + Upload: 'Upload', + Market_Plugins: 'Market Plugin', + My_Plugins: 'My Plugins', + Del_Knowledge_Tips: 'Do you want delete the knowledge', + Del_Document_Tips: 'Do you want delete the Document', + Tips: 'Tips', + Limit_Upload_File_Count_Tips: 'Only one file can be uploaded at a time', }, }, zh: { @@ -142,7 +157,8 @@ const resources = { space: '知识库', Vector: '向量', Owner: '创建人', - Docs: '文档数', + Count: '文档数', + File_type_Invalid: '文件类型错误', Knowledge_Space_Config: '知识库配置', Choose_a_Datasource_type: '选择数据源类型', Setup_the_Datasource: '设置数据源', @@ -181,6 +197,7 @@ const resources = { Delete: '删除', Operation: '操作', Submit: '提交', + close: '关闭', Chunks: '切片', Content: '内容', Meta_Data: '元数据', @@ -228,6 +245,9 @@ const resources = { submit: '提交', start_model_success: '启动模型成功', download_model_tip: '请先下载模型!', + Plugins: '插件列表', + try_again: '刷新重试', + no_data: '暂无数据', Prompt: '提示语', Open_Sidebar: '展开', cancel: '取消', @@ -266,6 +286,17 @@ const resources = { Copry_error: '复制失败', Click_Select: '点击选择', Quick_Start: '快速开始', + Select_Plugins: '选择插件', + Search: '搜索', + Reset: '重置', + Update_From_Github: '更新Github插件', + Upload: '上传', + Market_Plugins: '插件市场', + My_Plugins: '我的插件', + Del_Knowledge_Tips: '你确定删除该知识库吗', + Del_Document_Tips: '你确定删除该文档吗', + Tips: '提示', + Limit_Upload_File_Count_Tips: '一次只能上传一个文件', }, }, }; @@ -278,4 +309,6 @@ i18n.use(initReactI18next).init({ }, }); +export type I18nKeyMapper = (typeof resources)['en']['translation']; + export default i18n; diff --git a/client/api/request.ts b/client/api/request.ts index 302a60b..191f01b 100644 --- a/client/api/request.ts +++ b/client/api/request.ts @@ -1,7 +1,7 @@ import { AxiosRequestConfig } from 'axios'; import { GET, POST } from '.'; import { DbListResponse, DbSupportTypeResponse, PostDbParams, ChatFeedBackSchema } from '@/types/db'; -import { DialogueListResponse, IChatDialogueSchema, NewDialogueParam, SceneResponse, ChatHistoryResponse } from '@/types/chart'; +import { DialogueListResponse, IChatDialogueSchema, NewDialogueParam, SceneResponse, ChatHistoryResponse } from '@/types/chat'; import { IModelData, StartModelParams, BaseModelParams, SupportModel } from '@/types/model'; import { GetEditorSQLRoundRequest, @@ -11,6 +11,18 @@ import { PostEditorSQLRunParams, PostSQLEditorSubmitParams, } from '@/types/editor'; +import { PostAgentHubUpdateParams, PostAgentQueryParams, PostAgentPluginResponse, PostAgentMyPluginResponse } from '@/types/agent'; +import { + AddKnowledgeParams, + ArgumentsParams, + ChunkListParams, + DocumentParams, + IArguments, + IChunkList, + IDocument, + IDocumentResponse, + IKnowLedge, +} from '@/types/knowledge'; /** App */ export const postScenes = () => { @@ -104,6 +116,47 @@ export const getEditorSql = (id: string, round: string | number) => { }; /** knowledge */ +export const getArguments = (knowledgeName: string) => { + return POST(`/knowledge/${knowledgeName}/arguments`, {}); +}; +export const saveArguments = (knowledgeName: string, data: ArgumentsParams) => { + return POST(`/knowledge/${knowledgeName}/argument/save`, data); +}; + +export const getKnowledgeList = () => { + return POST>('/knowledge/space/list', {}); +}; +export const getDocumentList = (knowLedgeName: string, data: Record) => { + return POST, IDocumentResponse>(`/knowledge/${knowLedgeName}/document/list`, data); +}; + +export const addDocument = (knowledgeName: string, data: DocumentParams) => { + return POST(`/knowledge/${knowledgeName}/document/add`, data); +}; + +export const addKnowledge = (data: AddKnowledgeParams) => { + return POST>(`/knowledge/space/add`, data); +}; + +export const syncDocument = (knowLedgeName: string, data: Record>) => { + return POST>, string | null>(`/knowledge/${knowLedgeName}/document/sync`, data); +}; + +export const uploadDocument = (knowLedgeName: string, data: FormData) => { + return POST(`/knowledge/${knowLedgeName}/document/upload`, data); +}; + +export const getChunkList = (spaceName: string, data: ChunkListParams) => { + return POST(`/knowledge/${spaceName}/chunk/list`, data); +}; + +export const delDocument = (knowledgeName: string, data: Record) => { + return POST, null>(`/knowledge/${knowledgeName}/document/delete`, data); +}; + +export const delKnowledge = (data: Record) => { + return POST, null>(`/knowledge/space/delete`, data); +}; /** models */ export const getModelList = () => { @@ -122,6 +175,32 @@ export const getSupportModels = () => { return GET>('/api/v1/worker/model/params'); }; +/** Agent */ +export const postAgentQuery = (data: PostAgentQueryParams) => { + return POST('/api/v1/agent/query', data); +}; +export const postAgentHubUpdate = (data?: PostAgentHubUpdateParams) => { + return POST('/api/v1/agent/hub/update', data ?? { channel: '', url: '', branch: '', authorization: '' }); +}; +export const postAgentMy = (user?: string) => { + return POST('/api/v1/agent/my', undefined, { params: { user } }); +}; +export const postAgentInstall = (pluginName: string, user?: string) => { + return POST('/api/v1/agent/install', undefined, { params: { plugin_name: pluginName, user }, timeout: 60000 }); +}; +export const postAgentUninstall = (pluginName: string, user?: string) => { + return POST('/api/v1/agent/uninstall', undefined, { params: { plugin_name: pluginName, user }, timeout: 60000 }); +}; +export const postAgentUpload = (user = '', data: FormData, config?: Omit) => { + return POST('/api/v1/personal/agent/upload', data, { + params: { user }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + ...config, + }); +}; + /** chat feedback **/ export const getChatFeedBackSelect = () => { return GET>(`/api/v1/feedback/select`, undefined); diff --git a/client/api/tools/interceptors.ts b/client/api/tools/interceptors.ts index ccfbedc..012b086 100644 --- a/client/api/tools/interceptors.ts +++ b/client/api/tools/interceptors.ts @@ -20,6 +20,10 @@ export const apiInterceptors = (promise: Promise(); + + const [form] = Form.useForm(); + + const pagination = useMemo<{ pageNo: number; pageSize: number }>( + () => ({ + pageNo: 1, + pageSize: 20, + }), + [], + ); + + const { + data: agents = [], + loading, + refresh, + } = useRequest(async () => { + const queryParams: PostAgentQueryParams = { + page_index: pagination.pageNo, + page_size: pagination.pageSize, + filter: form.getFieldsValue(), + }; + const [err, res] = await apiInterceptors(postAgentQuery(queryParams)); + setIsError(!!err); + return res?.datas ?? []; + }); + + const updateFromGithub = async () => { + try { + setUploading(true); + const [err] = await apiInterceptors(postAgentHubUpdate()); + if (err) return; + message.success('success'); + refresh(); + } finally { + setUploading(false); + } + }; + + const pluginAction = useCallback( + async (name: string, index: number, isInstall: boolean) => { + if (actionIndex) return; + setActionIndex(index); + const [err] = await apiInterceptors((isInstall ? postAgentInstall : postAgentUninstall)(name)); + if (!err) { + message.success('success'); + refresh(); + } + setActionIndex(undefined); + }, + [actionIndex, refresh], + ); + + const renderAction = useCallback( + (agent: IAgentPlugin, index: number) => { + if (index === actionIndex) { + return ; + } + return agent.installed ? ( + +
{ + pluginAction(agent.name, index, false); + }} + > + +
+
+ ) : ( + +
{ + pluginAction(agent.name, index, true); + }} + > + +
+
+ ); + }, + [actionIndex, pluginAction], + ); + + return ( + +
+ + + + + + + +
+ {!agents.length && !loading && } +
+ {agents.map((agent, index) => ( + +
{ + window.open(agent.storage_url, '_blank'); + }} + > + +
+ , + ]} + > + +

{agent.name}

+
+ {agent.author && {agent.author}} + {agent.version && v{agent.version}} + {agent.type && Type {agent.type}} + {agent.storage_channel && {agent.storage_channel}} + +

{agent.description}

+
+
+ ))} +
+
+ ); +} + +export default MarketPlugins; diff --git a/components/agent/my-plugins.tsx b/components/agent/my-plugins.tsx new file mode 100644 index 0000000..d7c394d --- /dev/null +++ b/components/agent/my-plugins.tsx @@ -0,0 +1,124 @@ +import { apiInterceptors, postAgentMy, postAgentUninstall, postAgentUpload } from '@/client/api'; +import { IMyPlugin } from '@/types/agent'; +import { useRequest } from 'ahooks'; +import { Button, Card, Spin, Tag, Tooltip, Upload, UploadProps, message } from 'antd'; +import { useCallback, useState } from 'react'; +import MyEmpty from '../common/MyEmpty'; +import { ClearOutlined, LoadingOutlined, UploadOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +function MyPlugins() { + const { t } = useTranslation(); + const [messageApi, contextHolder] = message.useMessage(); + + const [uploading, setUploading] = useState(false); + const [isError, setIsError] = useState(false); + const [actionIndex, setActionIndex] = useState(); + + const { + data = [], + loading, + refresh, + } = useRequest(async () => { + const [err, res] = await apiInterceptors(postAgentMy()); + setIsError(!!err); + return res ?? []; + }); + + const uninstall = async (name: string, index: number) => { + if (actionIndex) return; + setActionIndex(index); + const [err] = await apiInterceptors(postAgentUninstall(name)); + message[err ? 'error' : 'success'](err ? 'failed' : 'success'); + !err && refresh(); + setActionIndex(undefined); + }; + + const renderAction = useCallback( + (item: IMyPlugin, index: number) => { + if (index === actionIndex) { + return ; + } + return ( + +
{ + uninstall(item.name, index); + }} + > + +
+
+ ); + }, + [actionIndex], + ); + + const onChange: UploadProps['onChange'] = async (info) => { + if (!info) { + message.error('Please select the *.zip,*.rar file'); + return; + } + try { + const file = info.file; + setUploading(true); + const formData = new FormData(); + formData.append('doc_file', file as any); + messageApi.open({ content: `Uploading ${file.name}`, type: 'loading', duration: 0 }); + const [err] = await apiInterceptors(postAgentUpload(undefined, formData, { timeout: 60000 })); + if (err) return; + message.success('success'); + refresh(); + } catch (e: any) { + message.error(e?.message || 'Upload Error'); + } finally { + setUploading(false); + messageApi.destroy(); + } + }; + + return ( + + {contextHolder} +
+ false} + name="file" + accept=".zip,.rar" + multiple={false} + onChange={onChange} + showUploadList={{ + showDownloadIcon: false, + showPreviewIcon: false, + showRemoveIcon: false, + }} + itemRender={() => <>} + > + + +
+ {!data.length && !loading && } +
+ {data.map((item, index) => ( + + +

{item.name}

+
+ {item.version && v{item.version}} + {item.type && Type {item.type}} + +

{item.description}

+
+
+ ))} +
+
+ ); +} + +export default MyPlugins; diff --git a/components/chart/bar-chart.tsx b/components/chart/bar-chart.tsx index dfb51e8..7e102fd 100644 --- a/components/chart/bar-chart.tsx +++ b/components/chart/bar-chart.tsx @@ -1,4 +1,4 @@ -import { ChartData } from '@/types/chart'; +import { ChartData } from '@/types/chat'; import { Column } from '@antv/g2plot'; import { Card, CardContent, Typography } from '@mui/joy'; import { useEffect, useRef } from 'react'; diff --git a/components/chart/index.tsx b/components/chart/index.tsx index 45092de..a4ed444 100644 --- a/components/chart/index.tsx +++ b/components/chart/index.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, Typography } from '@mui/joy'; import BarChart from './bar-chart'; import LineChart from './line-chart'; import TableChart from './table-chart'; -import { ChartData } from '@/types/chart'; +import { ChartData } from '@/types/chat'; import { useMemo } from 'react'; type Props = { diff --git a/components/chart/line-chart.tsx b/components/chart/line-chart.tsx index 51766d1..586eaec 100644 --- a/components/chart/line-chart.tsx +++ b/components/chart/line-chart.tsx @@ -1,4 +1,4 @@ -import { ChartData } from '@/types/chart'; +import { ChartData } from '@/types/chat'; import { Card, CardContent, Typography } from '@mui/joy'; import { useEffect, useRef } from 'react'; import { Line } from '@antv/g2plot'; diff --git a/components/chart/table-chart.tsx b/components/chart/table-chart.tsx index 56ed605..8d04257 100644 --- a/components/chart/table-chart.tsx +++ b/components/chart/table-chart.tsx @@ -1,4 +1,4 @@ -import { ChartData } from '@/types/chart'; +import { ChartData } from '@/types/chat'; import { Card, CardContent, Typography, Table } from '@mui/joy'; import { groupBy } from 'lodash'; diff --git a/components/chat/chat-container.tsx b/components/chat/chat-container.tsx index 8659a16..2f03e5e 100644 --- a/components/chat/chat-container.tsx +++ b/components/chat/chat-container.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useAsyncEffect } from 'ahooks'; import useChat from '@/hooks/use-chat'; import Completion from './completion'; -import { ChartData, ChatHistoryResponse } from '@/types/chart'; +import { ChartData, ChatHistoryResponse } from '@/types/chat'; import { apiInterceptors, getChatHistory } from '@/client/api'; import { ChatContext } from '@/app/chat-context'; import Header from './header'; diff --git a/components/chat/chat-content.tsx b/components/chat/chat-content.tsx deleted file mode 100644 index 7f97a61..0000000 --- a/components/chat/chat-content.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { PropsWithChildren, memo, useContext } from 'react'; -import { CodeOutlined, CopyOutlined, LinkOutlined, RobotOutlined, SyncOutlined, UserOutlined } from '@ant-design/icons'; -import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import ReactMarkdown from 'react-markdown'; -import { IChatDialogueMessageSchema } from '@/types/chart'; -import rehypeRaw from 'rehype-raw'; -import classNames from 'classnames'; -import { Button, Image, Tag, message } from 'antd'; -import { renderModelIcon } from './header/model-selector'; -import { ChatContext } from '@/app/chat-context'; -import copy from 'copy-to-clipboard'; - -interface Props { - content: Omit & { - context: - | string - | { - template_name: string; - template_introduce: string; - }; - }; - isChartChat?: boolean; - onLinkClick: () => void; -} - -const markdownComponents: Parameters['0']['components'] = { - code({ inline, node, className, children, style, ...props }) { - const match = /language-(\w+)/.exec(className || ''); - return !inline && match ? ( -
-
- ) : ( - - {children} - - ); - }, - ul({ children }) { - return
    {children}
; - }, - ol({ children }) { - return
    {children}
; - }, - li({ children, ordered }) { - return
  • {children}
  • ; - }, - table({ children }) { - return ( - {children}
    - ); - }, - thead({ children }) { - return {children}; - }, - th({ children }) { - return {children}; - }, - td({ children }) { - return {children}; - }, - h1({ children }) { - return

    {children}

    ; - }, - h2({ children }) { - return

    {children}

    ; - }, - h3({ children }) { - return

    {children}

    ; - }, - h4({ children }) { - return

    {children}

    ; - }, - a({ children, href }) { - return ( - - ); - }, - img({ src, alt }) { - return ( -
    - {alt}} color="processing"> - Image Loading... - - } - fallback="/images/fallback.png" - /> -
    - ); - }, - blockquote({ children }) { - return ( -
    - {children} -
    - ); - }, -}; - -function ChatContent({ children, content, isChartChat, onLinkClick }: PropsWithChildren) { - const { scene } = useContext(ChatContext); - - const { context, model_name, role } = content; - const isRobot = role === 'view'; - - const [contextMsg, relation] = typeof context === 'string' ? context.split('\trelations:') : [context]; - const relations = (relation && typeof relation === 'string' ? relation : null)?.split(','); - - return ( - <> -
    -
    - {isRobot ? renderModelIcon(model_name) || : } -
    -
    - {/* User Input */} - {!isRobot && typeof context === 'string' && context} - {/* Render Report */} - {isRobot && isChartChat && typeof context === 'object' && ( -
    - {`[${context.template_name}]: `} - - - {context.template_introduce || 'More Details'} - -
    - )} - {/* Markdown */} - {isRobot && typeof contextMsg === 'string' && ( - - {contextMsg - .replaceAll('\\n', '\n') - .replace(/]+)>/gi, '') - .replace(/]+)>/gi, '')} - - )} - {typeof relations === 'object' && !!relations?.length && ( -
    - {relations?.map((value, index) => ( - - {value} - - ))} -
    - )} - - {children} - - - ); -} - -export default memo(ChatContent); diff --git a/components/chat/chat-content/config.tsx b/components/chat/chat-content/config.tsx new file mode 100644 index 0000000..0dc544a --- /dev/null +++ b/components/chat/chat-content/config.tsx @@ -0,0 +1,112 @@ +import { CopyOutlined, LinkOutlined, SyncOutlined } from '@ant-design/icons'; +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import ReactMarkdown from 'react-markdown'; +import { Button, Image, Tag, message } from 'antd'; +import copy from 'copy-to-clipboard'; + +type MarkdownComponent = Parameters['0']['components']; + +const basicComponents: MarkdownComponent = { + code({ inline, node, className, children, style, ...props }) { + const match = /language-(\w+)/.exec(className || ''); + return !inline && match ? ( +
    +
    + ) : ( + + {children} + + ); + }, + ul({ children }) { + return
      {children}
    ; + }, + ol({ children }) { + return
      {children}
    ; + }, + li({ children, ordered }) { + return
  • {children}
  • ; + }, + table({ children }) { + return ( +
    {children}
    + ); + }, + thead({ children }) { + return {children}; + }, + th({ children }) { + return {children}; + }, + td({ children }) { + return {children}; + }, + h1({ children }) { + return

    {children}

    ; + }, + h2({ children }) { + return

    {children}

    ; + }, + h3({ children }) { + return

    {children}

    ; + }, + h4({ children }) { + return

    {children}

    ; + }, + a({ children, href }) { + return ( + + ); + }, + img({ src, alt }) { + return ( +
    + {alt}} color="processing"> + Image Loading... + + } + fallback="/images/fallback.png" + /> +
    + ); + }, + blockquote({ children }) { + return ( +
    + {children} +
    + ); + }, +}; + +const extraComponents: MarkdownComponent = {}; + +const markdownComponents = { + ...basicComponents, + ...extraComponents, +}; + +export default markdownComponents; diff --git a/components/chat/chat-content/index.tsx b/components/chat/chat-content/index.tsx new file mode 100644 index 0000000..73330f1 --- /dev/null +++ b/components/chat/chat-content/index.tsx @@ -0,0 +1,180 @@ +import { PropsWithChildren, ReactNode, memo, useContext, useMemo } from 'react'; +import { CheckOutlined, ClockCircleOutlined, CloseOutlined, CodeOutlined, LoadingOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons'; +import ReactMarkdown from 'react-markdown'; +import { IChatDialogueMessageSchema } from '@/types/chat'; +import rehypeRaw from 'rehype-raw'; +import classNames from 'classnames'; +import { Tag } from 'antd'; +import { renderModelIcon } from '../header/model-selector'; +import { ChatContext } from '@/app/chat-context'; +import markdownComponents from './config'; + +interface Props { + content: Omit & { + context: + | string + | { + template_name: string; + template_introduce: string; + }; + }; + isChartChat?: boolean; + onLinkClick: () => void; +} + +type MarkdownComponent = Parameters['0']['components']; + +type DBGPTView = { + name: string; + status: 'todo' | 'runing' | 'failed' | 'completed' | (string & {}); + result?: string; + err_msg?: string; +}; + +const pluginViewStatusMapper: Record = { + todo: { + bgClass: 'bg-gray-500', + icon: , + }, + runing: { + bgClass: 'bg-blue-500', + icon: , + }, + failed: { + bgClass: 'bg-red-500', + icon: , + }, + completed: { + bgClass: 'bg-green-500', + icon: , + }, +}; + +function formatMarkdownVal(val: string) { + return val + .replaceAll('\\n', '\n') + .replace(/]+)>/gi, '') + .replace(/]+)>/gi, ''); +} + +function ChatContent({ children, content, isChartChat, onLinkClick }: PropsWithChildren) { + const { scene } = useContext(ChatContext); + + const { context, model_name, role } = content; + const isRobot = role === 'view'; + + const { relations, value, cachePlguinContext } = useMemo<{ relations: string[]; value: string; cachePlguinContext: DBGPTView[] }>(() => { + if (typeof context !== 'string') { + return { + relations: [], + value: '', + cachePlguinContext: [], + }; + } + const [value, relation] = context.split('\trelations:'); + const relations = relation ? relation.split(',') : []; + const cachePlguinContext: DBGPTView[] = []; + + let cacheIndex = 0; + const result = value.replace(/]*>[^<]*<\/dbgpt-view>/gi, (matchVal) => { + try { + console.log(matchVal); + const pluginVal = matchVal.replaceAll('\n', '\\n').replace(/<[^>]*>|<\/[^>]*>/gm, ''); + const pluginContext = JSON.parse(pluginVal) as DBGPTView; + const replacement = `${cacheIndex}`; + + cachePlguinContext.push({ + ...pluginContext, + result: formatMarkdownVal(pluginContext.result ?? ''), + }); + cacheIndex++; + + return replacement; + } catch (e) { + console.log((e as any).message, e); + return matchVal; + } + }); + return { + relations, + cachePlguinContext, + value: result, + }; + }, [context]); + + const extraMarkdownComponents = useMemo( + () => ({ + 'custom-view'({ children }) { + const index = +children.toString(); + if (!cachePlguinContext[index]) { + return children; + } + const { name, status, err_msg, result } = cachePlguinContext[index]; + const { bgClass, icon } = pluginViewStatusMapper[status] ?? {}; + return ( +
    +
    + {name} + {icon} +
    + {result ? ( +
    + + {result ?? ''} + +
    + ) : ( +
    {err_msg}
    + )} +
    + ); + }, + }), + [context, cachePlguinContext], + ); + + return ( +
    +
    + {isRobot ? renderModelIcon(model_name) || : } +
    +
    + {/* User Input */} + {!isRobot && typeof context === 'string' && context} + {/* Render Report */} + {isRobot && isChartChat && typeof context === 'object' && ( +
    + {`[${context.template_name}]: `} + + + {context.template_introduce || 'More Details'} + +
    + )} + {/* Markdown */} + {isRobot && typeof context === 'string' && ( + + {formatMarkdownVal(value)} + + )} + {!!relations?.length && ( +
    + {relations?.map((value, index) => ( + + {value} + + ))} +
    + )} +
    + {children} +
    + ); +} + +export default memo(ChatContent); diff --git a/components/chat/completion.tsx b/components/chat/completion.tsx index e9b37cb..9d689ff 100644 --- a/components/chat/completion.tsx +++ b/components/chat/completion.tsx @@ -4,7 +4,7 @@ import MonacoEditor from './monaco-editor'; import ChatContent from './chat-content'; import ChatFeedback from './chat-feedback'; import { ChatContext } from '@/app/chat-context'; -import { IChatDialogueMessageSchema } from '@/types/chart'; +import { IChatDialogueMessageSchema } from '@/types/chat'; import classNames from 'classnames'; import { Empty, Modal, message, Tooltip } from 'antd'; import { renderModelIcon } from './header/model-selector'; @@ -24,7 +24,7 @@ type Props = { }; const Completion = ({ messages, onSubmit }: Props) => { - const { dbParam, currentDialogue, scene, model, refreshDialogList, chatId } = useContext(ChatContext); + const { dbParam, currentDialogue, scene, model, refreshDialogList, chatId, agentList } = useContext(ChatContext); const { t } = useTranslation(); const searchParams = useSearchParams(); @@ -39,12 +39,23 @@ const Completion = ({ messages, onSubmit }: Props) => { const isChartChat = useMemo(() => scene === 'chat_dashboard', [scene]); + const selectParam = useMemo(() => { + switch (scene) { + case 'chat_agent': + return agentList.join(','); + case 'chat_excel': + return currentDialogue?.select_param; + default: + return spaceNameOriginal || dbParam; + } + }, [scene, agentList, currentDialogue, dbParam, spaceNameOriginal]); + const handleChat = async (message: string) => { if (isLoading || !message.trim()) return; try { setIsLoading(true); await onSubmit(message, { - select_param: scene === 'chat_excel' ? currentDialogue?.select_param : spaceNameOriginal || dbParam, + select_param: selectParam ?? '', }); } finally { setIsLoading(false); diff --git a/components/chat/header/agent-selector.tsx b/components/chat/header/agent-selector.tsx new file mode 100644 index 0000000..c851c3a --- /dev/null +++ b/components/chat/header/agent-selector.tsx @@ -0,0 +1,39 @@ +import { ChatContext } from '@/app/chat-context'; +import { apiInterceptors, postAgentMy } from '@/client/api'; +import { useRequest } from 'ahooks'; +import { Select } from 'antd'; +import { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; + +function AgentSelector() { + const { t } = useTranslation(); + const { agentList, setAgentList } = useContext(ChatContext); + + const { data = [] } = useRequest(async () => { + const [, res] = await apiInterceptors(postAgentMy()); + if (res && res.length) { + setAgentList?.([res[0].name]); + } + return res ?? []; + }); + + if (!data.length) return null; + + return ( + { onChange?.(val); }} diff --git a/components/chat/mode-tab/index.tsx b/components/chat/mode-tab/index.tsx index 32a6528..f91ecff 100644 --- a/components/chat/mode-tab/index.tsx +++ b/components/chat/mode-tab/index.tsx @@ -1,8 +1,3 @@ -/** - * Preview and Editor tab component - */ -import DashboardIcon from '@mui/icons-material/Dashboard'; -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import './index.css'; import { useContext } from 'react'; import { ChatContext } from '@/app/chat-context'; diff --git a/components/common/MyEmpty.tsx b/components/common/MyEmpty.tsx new file mode 100644 index 0000000..acffd79 --- /dev/null +++ b/components/common/MyEmpty.tsx @@ -0,0 +1,31 @@ +import { Button, Empty } from 'antd'; +import { useTranslation } from 'react-i18next'; + +interface Props { + error?: boolean; + description?: string; + refresh?: () => void; +} + +function MyEmpty({ error, description, refresh }: Props) { + const { t } = useTranslation(); + + return ( + + {t('try_again')} + + ) : ( + description ?? t('no_data') + ) + } + /> + ); +} + +export default MyEmpty; diff --git a/components/datastores/space-parameter.tsx b/components/datastores/space-parameter.tsx deleted file mode 100644 index 3c43695..0000000 --- a/components/datastores/space-parameter.tsx +++ /dev/null @@ -1,370 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useRequest } from 'ahooks'; -import { sendSpacePostRequest } from '@/utils/request'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import MiscellaneousServicesIcon from '@mui/icons-material/MiscellaneousServices'; -import ManageSearchIcon from '@mui/icons-material/ManageSearch'; -import TipsAndUpdatesIcon from '@mui/icons-material/TipsAndUpdates'; -import { Popover, Tabs, message } from 'antd'; -import { Button, Sheet, Modal, Box, Stack, Input, Textarea } from '@/lib/mui'; -import Image from 'next/image'; -import { useTranslation } from 'react-i18next'; - -const SpaceParameter = ({ spaceName }: { spaceName: string }) => { - const [isParameterModalShow, setIsParameterModalShow] = useState(false); - const [newSpaceArguments, setNewSpaceArguments] = useState({}); - const { t } = useTranslation(); - - const { data: spaceArguments } = useRequest(() => sendSpacePostRequest(`/knowledge/${spaceName}/arguments`), { - onSuccess(result: any) { - setNewSpaceArguments(result.data); - }, - }); - const items = [ - { - key: 'Embedding', - label: ( - - - {t('Embedding')} - - ), - children: ( - - - - - {t('topk')} - - - - - - { - newSpaceArguments.embedding.topk = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - > - - - - - {t('recall_score')} - - - - - - { - newSpaceArguments.embedding.recall_score = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - disabled - > - - - - - - - {t('recall_type')} - - - - - - { - newSpaceArguments.embedding.recall_type = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - disabled - > - - - - - {t('model')} - - - - - - { - newSpaceArguments.embedding.model = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - disabled - startDecorator={} - > - - - - - - - {t('chunk_size')} - - - - - - { - newSpaceArguments.embedding.chunk_size = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - > - - - - - {t('chunk_overlap')} - - - - - - { - newSpaceArguments.embedding.chunk_overlap = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - > - - - - - ), - }, - { - key: 'Prompt', - label: ( - - - {t('Prompt')} - - ), - children: ( - - - - {t('scene')} - - - - - - - - - - - {t('template')} - - - - - - - - - - - {t('max_token')} - - - - - - { - newSpaceArguments.prompt.max_token = e.target.value; - setNewSpaceArguments({ ...newSpaceArguments }); - }} - > - - - - ), - }, - ]; - return ( - <> - - setIsParameterModalShow(false)} - > - - - - - - - - - ); -}; - -export default SpaceParameter; diff --git a/components/knowledge/add-modal.tsx b/components/knowledge/add-modal.tsx new file mode 100644 index 0000000..43bd878 --- /dev/null +++ b/components/knowledge/add-modal.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { Steps, Modal } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import { IKnowLedge } from '@/types/knowledge'; +import AddKnowledge from './knowledge-form'; +import AddDatasource from './datasource-form'; + +interface IProps { + setDocuments?: (documents: any) => void; + isAddShow: boolean; + setIsAddShow: (isAddShow: boolean) => void; + setKnowledgeSpaceList?: (list: Array) => void; + type?: 'knowledge' | 'document'; + knowLedge?: IKnowLedge; + fetchDocuments?: () => void; + fetchKnowledge?: () => void; + syncDocuments?: (knowledgeName: string, id: number) => void; +} + +export default function AddModal(props: IProps) { + const { setIsAddShow, isAddShow, type, knowLedge, fetchDocuments, fetchKnowledge, syncDocuments } = props; + const { t } = useTranslation(); + + const addKnowledgeSteps = [{ title: t('Knowledge_Space_Config') }, { title: t('Choose_a_Datasource_type') }, { title: t('Setup_the_Datasource') }]; + + const addDocumentSteps = [{ title: t('Choose_a_Datasource_type') }, { title: t('Setup_the_Datasource') }]; + + const [documentType, setDocumentType] = useState(''); + const [activeStep, setActiveStep] = useState(0); + + const handleAddKnowledge = async () => { + setActiveStep(1); + fetchKnowledge?.(); + }; + const handleChooseType = (item: any) => { + setDocumentType(item.type); + + setActiveStep(type === 'knowledge' ? 2 : 1); + }; + + const handleBackBtn = () => { + setActiveStep(type === 'knowledge' ? 1 : 0); + }; + + const renderStepContent = () => { + const renderStep = type === 'document' ? activeStep + 1 : activeStep; + if (renderStep === 0) { + return ; + } + return ( + + ); + }; + + return ( + { + setIsAddShow(false); + }} + width={1000} + afterClose={() => { + setActiveStep(0); + }} + footer={null} + > + + {renderStepContent()} + + ); +} diff --git a/components/knowledge/arguments.tsx b/components/knowledge/arguments.tsx new file mode 100644 index 0000000..c21eb73 --- /dev/null +++ b/components/knowledge/arguments.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Tabs, Button, Input, Form, Col, Row } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import { AlertFilled, FileSearchOutlined } from '@ant-design/icons'; +import { apiInterceptors, getArguments, saveArguments } from '@/client/api'; +import { IArguments } from '@/types/knowledge'; + +const { TextArea } = Input; + +interface IProps { + knowledge: any; + argumentsShow: boolean; + setArgumentsShow: (argumentsShow: boolean) => void; +} + +export default function ArgumentsModal({ knowledge, argumentsShow, setArgumentsShow }: IProps) { + const { t } = useTranslation(); + const [newSpaceArguments, setNewSpaceArguments] = useState(); + + const fetchArguments = async () => { + const [_, data] = await apiInterceptors(getArguments(knowledge.name)); + setNewSpaceArguments(data); + }; + + useEffect(() => { + fetchArguments(); + }, []); + + const renderEmbeddingForm = () => { + return ( + +
    + tooltip={t(`the_top_k_vectors`)} rules={[{ required: true }]} label={t('topk')} name={['embedding', 'topk']}> + + + + + + tooltip={t(`Set_a_threshold_score`)} + rules={[{ required: true }]} + label={t('recall_score')} + name={['embedding', 'recall_score']} + > + + + + + tooltip={t(`Recall_Type`)} rules={[{ required: true }]} label={t('recall_type')} name={['embedding', 'recall_type']}> + + + + + tooltip={t(`A_model_used`)} rules={[{ required: true }]} label={t('model')} name={['embedding', 'model']}> + + + + + + tooltip={t(`The_size_of_the_data_chunks`)} + rules={[{ required: true }]} + label={t('chunk_size')} + name={['embedding', 'chunk_size']} + > + + + + + + tooltip={t(`The_amount_of_overlap`)} + rules={[{ required: true }]} + label={t('chunk_overlap')} + name={['embedding', 'chunk_overlap']} + > + + + + + ); + }; + + const renderPromptForm = () => { + return ( + <> + tooltip={t(`A_contextual_parameter`)} label={t('scene')} name={['prompt', 'scene']}> +