feat: Front-end engineering replacement

This commit is contained in:
lhwan
2024-08-21 09:39:27 +08:00
parent 10f77ec2fd
commit 77baf7e1fa
443 changed files with 3545 additions and 24735 deletions

View File

@@ -1 +0,0 @@
API_BASE_URL=http://127.0.0.1:5000

View File

@@ -1,9 +0,0 @@
{
"tabWidth": 2,
"printWidth": 150,
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"jsxSingleQuote": false,
"arrowParens": "always"
}

View File

@@ -36,6 +36,7 @@ Also, it is a **LLM to Vision** solution.
### Prerequisites
- [Node.js](https://nodejs.org/) >= 16
- [npm](https://npmjs.com/) >= 8
- [yarn](https://yarnpkg.com/) >= 1.22
- Supported OSes: Linux, macOS and Windows
@@ -43,6 +44,7 @@ Also, it is a **LLM to Vision** solution.
```sh
# Install dependencies
npm install
yarn install
```
@@ -54,12 +56,14 @@ edit the `API_BASE_URL` to the real address
```sh
# development model
npm run dev
yarn dev
```
## 🚀 Use In DB-GPT
```sh
npm run compile
yarn compile
# copy compile file to DB-GPT static file dictory
@@ -90,4 +94,40 @@ Enjoy using DB-GPT-Web to build stunning UIs for your AI and GPT projects.
For any queries or issues, feel free to open an [issue](https://github.com/eosphoros-ai/DB-GPT-Web/issues) on the repository.
Happy coding! 😊
Happy coding! 😊
## antdbgptweb installation
### deploy in local environment:
1. 在 /etc/hosts文件增加一行, 必须配置本地xxx.alipay.net否则无法接入线下的登陆系统
```
127.0.0.1 local.alipay.net
```
2. 在.env文件增加下面配置
deploy in local environment:
```
ANT_BUC_GET_USER_URL='http://antbuservice.stable.alipay.net/pub/getLoginUser.json?appName=antdbgpt'
ANT_BUC_NOT_LOGIN_URL='http://antbuservice.stable.alipay.net/pub/userNotLogin.htm?appName=antdbgpt&sourceUrl='
API_BASE_URL='http://local.alipay.net:5000'
```
OR modify file next.config.js
```
env: {
API_BASE_URL: process.env.API_BASE_URL || "http://local.alipay.net:5000",
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
ANT_BUC_GET_USER_URL: 'http://antbuservice.stable.alipay.net/pub/getLoginUser.json?appName=antdbgpt',
ANT_BUC_NOT_LOGIN_URL: 'http://antbuservice.stable.alipay.net/pub/userNotLogin.htm?appName=antdbgpt&sourceUrl='
},
```
deploy in production environment:
```
ANT_BUC_GET_USER_URL=https://antbuservice.alipay.com/pub/getLoginUser.json?appName=antdbgpt
ANT_BUC_NOT_LOGIN_URL=https://antbuservice.alipay.com/pub/userNotLogin.htm?appName=antdbgpt&sourceUrl=
```

View File

@@ -1,9 +1,11 @@
import { createContext, useEffect, useMemo, useState } from 'react';
import { apiInterceptors, getDialogueList, getUsableModels } from '@/client/api';
import { useRequest } from 'ahooks';
import { apiInterceptors, getDialogueList, getUsableModels, queryAdminList } from '@/client/api';
import { ChatHistoryResponse, DialogueListResponse, IChatDialogueSchema } from '@/types/chat';
import { UserInfoResponse } from '@/types/userinfo';
import { getUserId } from '@/utils';
import { STORAGE_THEME_KEY } from '@/utils/constants/index';
import { useRequest } from 'ahooks';
import { useSearchParams } from 'next/navigation';
import { STORAGE_THEME_KEY } from '@/utils';
import { createContext, useEffect, useMemo, useState } from 'react';
type ThemeMode = 'dark' | 'light';
@@ -14,8 +16,8 @@ interface IChatContext {
scene: IChatDialogueSchema['chat_mode'] | (string & {});
chatId: string;
model: string;
modelList: string[];
dbParam?: string;
modelList: Array<string>;
agent: string;
dialogueList?: DialogueListResponse;
setAgent?: (val: string) => void;
@@ -24,13 +26,19 @@ interface IChatContext {
setIsContract: (val: boolean) => void;
setIsMenuExpand: (val: boolean) => void;
setDbParam: (val: string) => void;
queryDialogueList: () => void;
refreshDialogList: () => void;
currentDialogue?: DialogueListResponse[0];
history: ChatHistoryResponse;
setHistory: (val: ChatHistoryResponse) => void;
docId?: number;
setDocId: (docId: number) => void;
// 当前对话信息
currentDialogInfo: {
chat_scene: string;
app_code: string;
};
setCurrentDialogInfo: (val: { chat_scene: string; app_code: string }) => void;
adminList: UserInfoResponse[];
refreshDialogList?: any;
}
function getDefaultTheme(): ThemeMode {
@@ -43,8 +51,8 @@ const ChatContext = createContext<IChatContext>({
mode: 'light',
scene: '',
chatId: '',
modelList: [],
model: '',
modelList: [],
dbParam: undefined,
dialogueList: [],
agent: '',
@@ -53,13 +61,18 @@ const ChatContext = createContext<IChatContext>({
setIsContract: () => {},
setIsMenuExpand: () => {},
setDbParam: () => void 0,
queryDialogueList: () => {},
refreshDialogList: () => {},
setMode: () => void 0,
history: [],
setHistory: () => {},
docId: undefined,
setDocId: () => {},
currentDialogInfo: {
chat_scene: '',
app_code: '',
},
setCurrentDialogInfo: () => {},
adminList: [],
refreshDialogList: () => {},
});
const ChatContextProvider = ({ children }: { children: React.ReactElement }) => {
@@ -67,7 +80,6 @@ const ChatContextProvider = ({ children }: { children: React.ReactElement }) =>
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<string>('');
const [isMenuExpand, setIsMenuExpand] = useState<boolean>(scene !== 'chat_dashboard');
@@ -76,51 +88,66 @@ const ChatContextProvider = ({ children }: { children: React.ReactElement }) =>
const [history, setHistory] = useState<ChatHistoryResponse>([]);
const [docId, setDocId] = useState<number>();
const [mode, setMode] = useState<ThemeMode>('light');
// 管理员列表
const [adminList, setAdminList] = useState<UserInfoResponse[]>([]);
const {
run: queryDialogueList,
data: dialogueList = [],
refresh: refreshDialogList,
} = useRequest(
async () => {
const [, res] = await apiInterceptors(getDialogueList());
return res ?? [];
},
{
manual: true,
},
);
useEffect(() => {
if (dialogueList.length && scene === 'chat_agent') {
const agent = dialogueList.find((item) => item.conv_uid === chatId)?.select_param;
agent && setAgent(agent);
}
}, [dialogueList, scene, chatId]);
const [currentDialogInfo, setCurrentDialogInfo] = useState({
chat_scene: '',
app_code: '',
});
// 获取model
const { data: modelList = [] } = useRequest(async () => {
const [, res] = await apiInterceptors(getUsableModels());
return res ?? [];
});
// 获取管理员列表
const { run: queryAdminListRun } = useRequest(
async () => {
const [, res] = await apiInterceptors(queryAdminList({ role: 'admin' }));
return res ?? [];
},
{
onSuccess: (data) => {
setAdminList(data);
},
manual: true,
},
);
useEffect(() => {
if (getUserId()) {
queryAdminListRun();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryAdminListRun, getUserId()]);
useEffect(() => {
setMode(getDefaultTheme());
try {
const dialogInfo = JSON.parse(localStorage.getItem('cur_dialog_info') || '');
setCurrentDialogInfo(dialogInfo);
} catch (error) {
setCurrentDialogInfo({
chat_scene: '',
app_code: '',
});
}
}, []);
useEffect(() => {
setModel(modelList[0]);
}, [modelList, modelList?.length]);
const currentDialogue = useMemo(() => dialogueList.find((item: any) => item.conv_uid === chatId), [chatId, dialogueList]);
const contextValue = {
isContract,
isMenuExpand,
scene,
chatId,
modelList,
model,
modelList,
dbParam: dbParam || db_param,
dialogueList,
agent,
setAgent,
mode,
@@ -129,13 +156,13 @@ const ChatContextProvider = ({ children }: { children: React.ReactElement }) =>
setIsContract,
setIsMenuExpand,
setDbParam,
queryDialogueList,
refreshDialogList,
currentDialogue,
history,
setHistory,
docId,
setDocId,
currentDialogInfo,
setCurrentDialogInfo,
adminList,
};
return <ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>;
};

View File

@@ -1,464 +1,13 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
const en = {
Knowledge_Space: 'Knowledge',
space: 'space',
Vector: 'Vector',
Owner: 'Owner',
Count: 'Count',
File_type_Invalid: 'The file type is invalid',
Knowledge_Space_Config: 'Space Config',
Choose_a_Datasource_type: 'Datasource type',
Segmentation: 'Segmentation',
No_parameter: `No segementation parameter required.`,
Knowledge_Space_Name: 'Knowledge Space Name',
Please_input_the_name: 'Please input the name',
Please_input_the_owner: 'Please input the owner',
Please_select_file: 'Please select one file',
Description: 'Description',
Storage: 'Storage',
Please_input_the_description: 'Please input the description',
Please_select_the_storage: 'Please select the storage',
Please_select_the_domain_type: 'Please select the domain type',
Next: 'Next',
the_name_can_only_contain: 'the name can only contain numbers, letters, Chinese characters, "-" and "_"',
Text: 'Text',
'Fill your raw text': 'Fill your raw text',
URL: 'URL',
Fetch_the_content_of_a_URL: 'Fetch the content of a URL',
Document: 'Document',
Upload_a_document: 'Upload a document, document type can be PDF, CSV, Text, PowerPoint, Word, Markdown',
Name: 'Name',
Text_Source: 'Text Source(Optional)',
Please_input_the_text_source: 'Please input the text source',
Sync: 'Sync',
Back: 'Back',
Finish: 'Finish',
Web_Page_URL: 'Web Page URL',
Please_input_the_Web_Page_URL: 'Please input the Web Page URL',
Select_or_Drop_file: 'Select or Drop file',
Documents: 'Documents',
Chat: 'Chat',
Add_Datasource: 'Add Datasource',
View_Graph: 'View Graph',
Arguments: 'Arguments',
Type: 'Type',
Size: 'Size',
Last_Sync: 'Last Sync',
Status: 'Status',
Result: 'Result',
Details: 'Details',
Delete: 'Delete',
Operation: 'Operation',
Submit: 'Submit',
Chunks: 'Chunks',
Content: 'Content',
Meta_Data: 'Meta Data',
Please_select_a_file: 'Please select a file',
Please_input_the_text: 'Please input the text',
Embedding: 'Embedding',
topk: 'topk',
the_top_k_vectors: 'the top k vectors based on similarity score',
recall_score: 'recall_score',
Set_a_threshold_score: 'Set a threshold score for the retrieval of similar vectors',
recall_type: 'recall_type',
model: 'model',
A_model_used: 'A model used to create vector representations of text or other data',
Automatic: 'Automatic',
Process: 'Process',
Automatic_desc: 'Automatically set segmentation and preprocessing rules.',
chunk_size: 'chunk_size',
The_size_of_the_data_chunks: 'The size of the data chunks used in processing',
chunk_overlap: 'chunk_overlap',
The_amount_of_overlap: 'The amount of overlap between adjacent data chunks',
Prompt: 'Prompt',
scene: 'scene',
A_contextual_parameter: 'A contextual parameter used to define the setting or environment in which the prompt is being used',
template: 'template',
structure_or_format:
'A pre-defined structure or format for the prompt, which can help ensure that the AI system generates responses that are consistent with the desired style or tone.',
max_token: 'max_token',
max_iteration: 'max_iteration',
concurrency_limit: 'concurrency_limit',
The_maximum_number_of_tokens: 'The maximum number of tokens or words allowed in a prompt',
Theme: 'Theme',
Port: 'Port',
Username: 'Username',
Password: 'Password',
Remark: 'Remark',
Edit: 'Edit',
Database: 'Database',
Data_Source: 'Data Center',
Close_Sidebar: 'Fold',
Show_Sidebar: 'UnFold',
language: 'Language',
choose_model: 'Please choose a model',
data_center_desc: 'DB-GPT also offers a user-friendly data center management interface for efficient data maintenance.',
create_database: 'Create Database',
create_knowledge: 'Create Knowledge',
path: 'Path',
model_manage: 'Models',
stop_model_success: 'Stop model success',
create_model: 'Create Model',
model_select_tips: 'Please select a model',
language_select_tips: 'Please select a language',
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',
Add: 'Add',
Add_Success: 'Add Success',
Error_Message: 'Something Error',
Please_Input: 'Please Input',
Prompt_Info_Scene: 'Scene',
Prompt_Info_Sub_Scene: 'Sub Scene',
Prompt_Info_Name: 'Name',
Prompt_Info_Content: 'Content',
Public: 'Public',
Private: 'Private',
Lowest: 'Lowest',
Missed: 'Missed',
Lost: 'Lost',
Incorrect: 'Incorrect',
Verbose: 'Verbose',
Best: 'Best',
Rating: 'Rating',
Q_A_Category: 'Q&A Category',
Q_A_Rating: 'Q&A Rating',
feed_back_desc:
'0: No results\n' +
'1: Results exist, but they are irrelevant, the question is not understood\n' +
'2: Results exist, the question is understood, but it indicates that the question cannot be answered\n' +
'3: Results exist, the question is understood, and an answer is given, but the answer is incorrect\n' +
'4: Results exist, the question is understood, the answer is correct, but it is verbose and lacks a summary\n' +
'5: Results exist, the question is understood, the answer is correct, the reasoning is correct, and a summary is provided, concise and to the point\n',
input_count: 'Total input',
input_unit: 'characters',
Copy: 'Copy',
Copy_success: 'Content copied successfully',
Copy_nothing: 'Content copied is empty',
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 Space',
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',
To_Plugin_Market: 'Go to the Plugin Market',
Summary: 'Summary',
stacked_column_chart: 'Stacked Column',
column_chart: 'Column',
percent_stacked_column_chart: 'Percent Stacked Column',
grouped_column_chart: 'Grouped Column',
time_column: 'Time Column',
pie_chart: 'Pie',
line_chart: 'Line',
area_chart: 'Area',
stacked_area_chart: 'Stacked Area',
scatter_plot: 'Scatter',
bubble_chart: 'Bubble',
stacked_bar_chart: 'Stacked Bar',
bar_chart: 'Bar',
percent_stacked_bar_chart: 'Percent Stacked Bar',
grouped_bar_chart: 'Grouped Bar',
water_fall_chart: 'Waterfall',
table: 'Table',
multi_line_chart: 'Multi Line',
multi_measure_column_chart: 'Multi Measure Column',
multi_measure_line_chart: 'Multi Measure Line',
Advices: 'Advices',
Retry: 'Retry',
Load_more: 'load more',
new_chat: 'New Chat',
choice_agent_tip: 'Please choose an agent',
no_context_tip: 'Please enter your question',
Terminal: 'Terminal',
awel_flow: 'AWEL Flow',
save: 'Save',
add_node: 'Add Node',
no_node: 'No Node',
connect_warning: 'Nodes cannot be connected',
flow_modal_title: 'Save Flow',
flow_name: 'Flow Name',
flow_description: 'Flow Description',
flow_name_required: 'Please enter the flow name',
flow_description_required: 'Please enter the flow description',
save_flow_success: 'Save flow success',
delete_flow_confirm: 'Are you sure you want to delete this flow?',
related_nodes: 'Related Nodes',
add_resource: 'Add Resource',
team_modal: 'Work Modal',
App: 'App',
resource_name: 'Resource Name',
resource_type: 'Resource Type',
resource_value: 'Value',
resource_dynamic: 'Dynamic',
Please_input_the_work_modal: 'Please select the work modal',
available_resources: ' Available Resources',
edit_new_applications: 'Edit new applications',
collect: 'Collect',
collected: 'Collected',
create: 'Create',
Agents: 'Agents',
edit_application: 'edit application',
add_application: 'add application',
app_name: 'App Name',
LLM_strategy: 'LLM Strategy',
LLM_strategy_value: 'LLM Strategy Value',
resource: 'Resource',
operators: 'Operators',
Chinese: 'Chinese',
English: 'English',
refreshSuccess: 'Refresh Success',
Download: 'Download'
} as const;
import en from '@/locales/en';
import zh from '@/locales/zh';
export type I18nKeys = keyof typeof en;
export interface Resources {
interface Resources {
translation: Record<I18nKeys, string>;
}
const zh: Resources['translation'] = {
Knowledge_Space: '知识库',
space: '知识库',
Vector: '向量',
Owner: '创建人',
Count: '文档数',
File_type_Invalid: '文件类型错误',
Knowledge_Space_Config: '知识库配置',
Choose_a_Datasource_type: '知识库类型',
Segmentation: '分片',
No_parameter: '不需要配置分片参数',
Knowledge_Space_Name: '知识库名称',
Please_input_the_name: '请输入名称',
Please_input_the_owner: '请输入创建人',
Please_select_file: '请至少选择一个文件',
Description: '描述',
Storage: '存储类型',
Domain: '领域类型',
Please_input_the_description: '请输入描述',
Please_select_the_storage: '请选择存储类型',
Please_select_the_domain_type: '请选择领域类型',
Next: '下一步',
the_name_can_only_contain: '名称只能包含数字、字母、中文字符、-或_',
Text: '文本',
'Fill your raw text': '填写您的原始文本',
URL: '网址',
Fetch_the_content_of_a_URL: '获取 URL 的内容',
Document: '文档',
Upload_a_document: '上传文档文档类型可以是PDF、CSV、Text、PowerPoint、Word、Markdown',
Name: '名称',
Text_Source: '文本来源(可选)',
Please_input_the_text_source: '请输入文本来源',
Sync: '同步',
Back: '上一步',
Finish: '完成',
Web_Page_URL: '网页网址',
Please_input_the_Web_Page_URL: '请输入网页网址',
Select_or_Drop_file: '选择或拖拽文件',
Documents: '文档',
Chat: '对话',
Add_Datasource: '添加数据源',
View_Graph: '查看图谱',
Arguments: '参数',
Type: '类型',
Size: '切片',
Last_Sync: '上次同步时间',
Status: '状态',
Result: '结果',
Details: '明细',
Delete: '删除',
Operation: '操作',
Submit: '提交',
close: '关闭',
Chunks: '切片',
Content: '内容',
Meta_Data: '元数据',
Please_select_a_file: '请上传一个文件',
Please_input_the_text: '请输入文本',
Embedding: '嵌入',
topk: 'TopK',
the_top_k_vectors: '基于相似度得分的前 k 个向量',
recall_score: '召回分数',
Set_a_threshold_score: '设置相似向量检索的阈值分数',
recall_type: '召回类型',
model: '模型',
A_model_used: '用于创建文本或其他数据的矢量表示的模型',
Automatic: '自动切片',
Process: '切片处理',
Automatic_desc: '自动设置分割和预处理规则。',
chunk_size: '块大小',
The_size_of_the_data_chunks: '处理中使用的数据块的大小',
chunk_overlap: '块重叠',
The_amount_of_overlap: '相邻数据块之间的重叠量',
scene: '场景',
A_contextual_parameter: '用于定义使用提示的设置或环境的上下文参数',
template: '模板',
structure_or_format: '预定义的提示结构或格式,有助于确保人工智能系统生成与所需风格或语气一致的响应。',
max_token: '最大令牌',
max_iteration: '最大迭代',
concurrency_limit: '并发限制',
The_maximum_number_of_tokens: '提示中允许的最大标记或单词数',
Theme: '主题',
Port: '端口',
Username: '用户名',
Password: '密码',
Remark: '备注',
Edit: '编辑',
Database: '数据库',
Data_Source: '数据中心',
Close_Sidebar: '收起',
Show_Sidebar: '展开',
language: '语言',
choose_model: '请选择一个模型',
data_center_desc: 'DB-GPT支持数据库交互和基于文档的对话它还提供了一个用户友好的数据中心管理界面。',
create_database: '创建数据库',
create_knowledge: '创建知识库',
path: '路径',
model_manage: '模型管理',
stop_model_success: '模型停止成功',
create_model: '创建模型',
model_select_tips: '请选择一个模型',
submit: '提交',
start_model_success: '启动模型成功',
download_model_tip: '请先下载模型!',
Plugins: '插件列表',
try_again: '刷新重试',
no_data: '暂无数据',
Prompt: '提示语',
Open_Sidebar: '展开',
cancel: '取消',
Edit_Success: '编辑成功',
Add: '新增',
Add_Success: '新增成功',
Error_Message: '出错了',
Please_Input: '请输入',
Prompt_Info_Scene: '场景',
Prompt_Info_Sub_Scene: '次级场景',
Prompt_Info_Name: '名称',
Prompt_Info_Content: '内容',
Public: '公共',
Private: '私有',
Lowest: '渣渣',
Missed: '没理解',
Lost: '答不了',
Incorrect: '答错了',
Verbose: '较啰嗦',
Best: '真棒',
Rating: '评分',
Q_A_Category: '问答类别',
Q_A_Rating: '问答评分',
feed_back_desc:
'0: 无结果\n' +
'1: 有结果,但是在文不对题,没有理解问题\n' +
'2: 有结果,理解了问题,但是提示回答不了这个问题\n' +
'3: 有结果,理解了问题,并做出回答,但是回答的结果错误\n' +
'4: 有结果,理解了问题,回答结果正确,但是比较啰嗦,缺乏总结\n' +
'5: 有结果,理解了问题,回答结果正确,推理正确,并给出了总结,言简意赅\n',
input_count: '共计输入',
input_unit: '字',
Copy: '复制',
Copy_success: '内容复制成功',
Copy_nothing: '内容复制为空',
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: '一次只能上传一个文件',
To_Plugin_Market: '前往插件市场',
Summary: '总结',
stacked_column_chart: '堆叠柱状图',
column_chart: '柱状图',
percent_stacked_column_chart: '百分比堆叠柱状图',
grouped_column_chart: '簇形柱状图',
time_column: '簇形柱状图',
pie_chart: '饼图',
line_chart: '折线图',
area_chart: '面积图',
stacked_area_chart: '堆叠面积图',
scatter_plot: '散点图',
bubble_chart: '气泡图',
stacked_bar_chart: '堆叠条形图',
bar_chart: '条形图',
percent_stacked_bar_chart: '百分比堆叠条形图',
grouped_bar_chart: '簇形条形图',
water_fall_chart: '瀑布图',
table: '表格',
multi_line_chart: '多折线图',
multi_measure_column_chart: '多指标柱形图',
multi_measure_line_chart: '多指标折线图',
Advices: '自动推荐',
Retry: '重试',
Load_more: '加载更多',
new_chat: '创建会话',
choice_agent_tip: '请选择代理',
no_context_tip: '请输入你的问题',
Terminal: '终端',
awel_flow: 'AWEL 工作流',
save: '保存',
add_node: '添加节点',
no_node: '没有可编排节点',
connect_warning: '节点无法连接',
flow_modal_title: '保存工作流',
flow_name: '工作流名称',
flow_description: '工作流描述',
flow_name_required: '请输入工作流名称',
flow_description_required: '请输入工作流描述',
save_flow_success: '保存工作流成功',
delete_flow_confirm: '确定删除该工作流吗?',
related_nodes: '关联节点',
language_select_tips: '请选择语言',
add_resource: '添加资源',
team_modal: '工作模式',
App: '应用程序',
resource: '资源',
resource_name: '资源名',
resource_type: '资源类型',
resource_value: '参数',
resource_dynamic: '动态',
Please_input_the_work_modal: '请选择工作模式',
available_resources: '可用资源',
edit_new_applications: '编辑新的应用',
collect: '收藏',
collected: '已收藏',
create: '创建',
Agents: '智能体',
edit_application: '编辑应用',
add_application: '添加应用',
app_name: '应用名称',
LLM_strategy: '模型策略',
LLM_strategy_value: '模型策略参数',
operators: '算子',
Chinese: '中文',
English: '英文',
refreshSuccess: '刷新成功',
Download: '下载',
} as const;
i18n.use(initReactI18next).init({
resources: {
en: {

View File

@@ -1,3 +1,5 @@
import { getUserId } from '@/utils';
import { HEADER_USER_ID_KEY } from '@/utils/constants/index';
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios';
export type ResponseType<T = any> = {
@@ -39,8 +41,9 @@ const LONG_TIME_API: string[] = [
ins.interceptors.request.use((request) => {
const isLongTimeApi = LONG_TIME_API.some((item) => request.url && request.url.indexOf(item) >= 0);
if (!request.timeout) {
request.timeout = isLongTimeApi ? 60000 : 10000;
request.timeout = isLongTimeApi ? 60000 : 100000;
}
request.headers.set(HEADER_USER_ID_KEY, getUserId());
return request;
});
@@ -66,3 +69,10 @@ export const DELETE = <Params = any, Response = any, D = any>(url: string, param
export * from './tools';
export * from './request';
export * from './chat';
export * from './flow';
export * from './app';
export * from './knowledge';
export * from './user';
export * from './prompt';
export * from './evaluate';

View File

@@ -1,8 +1,23 @@
import { AxiosRequestConfig } from 'axios';
import { DELETE, GET, POST, PUT } from '.';
import { DbListResponse, DbSupportTypeResponse, PostDbParams, ChatFeedBackSchema, PostDbRefreshParams } from '@/types/db';
import { DialogueListResponse, IChatDialogueSchema, NewDialogueParam, SceneResponse, ChatHistoryResponse, FeedBack, IDB } from '@/types/chat';
import { IModelData, StartModelParams, BaseModelParams, SupportModel } from '@/types/model';
import {
GetDBGPTsListResponse,
PostAgentHubUpdateParams,
PostAgentMyPluginResponse,
PostAgentPluginResponse,
PostAgentQueryParams,
} from '@/types/agent';
import { GetAppInfoParams, IApp, IAgent, IAppData } from '@/types/app';
import {
ChatHistoryResponse,
DialogueListResponse,
FeedBack,
IChatDialogueSchema,
IDB,
NewDialogueParam,
SceneResponse,
UserParam,
UserParamResponse,
} from '@/types/chat';
import { ChatFeedBackSchema, DbListResponse, DbSupportTypeResponse, PostDbParams, PostDbRefreshParams } from '@/types/db';
import {
GetEditorSQLRoundRequest,
GetEditorySqlParams,
@@ -11,13 +26,7 @@ import {
PostEditorSQLRunParams,
PostSQLEditorSubmitParams,
} from '@/types/editor';
import {
PostAgentHubUpdateParams,
PostAgentQueryParams,
PostAgentPluginResponse,
PostAgentMyPluginResponse,
GetDBGPTsListResponse,
} from '@/types/agent';
import { IFlow, IFlowNode, IFlowUpdateParam, IFlowResponse } from '@/types/flow';
import {
AddKnowledgeParams,
ArgumentsParams,
@@ -33,16 +42,21 @@ import {
ISyncBatchResponse,
SpaceConfig,
} from '@/types/knowledge';
import { UpdatePromptParams, IPrompt, PromptParams } from '@/types/prompt';
import { IFlow, IFlowNode, IFlowResponse, IFlowUpdateParam } from '@/types/flow';
import { IAgent, IApp, IAppData, ITeamModal } from '@/types/app';
import { BaseModelParams, IModelData, StartModelParams, SupportModel } from '@/types/model';
import { AxiosRequestConfig } from 'axios';
import { DELETE, GET, POST, PUT } from '.';
/** App */
export const postScenes = () => {
return POST<null, Array<SceneResponse>>('/api/v1/chat/dialogue/scenes');
};
export const newDialogue = (data: NewDialogueParam) => {
return POST<NewDialogueParam, IChatDialogueSchema>('/api/v1/chat/dialogue/new', data);
return POST<NewDialogueParam, IChatDialogueSchema>(`/api/v1/chat/dialogue/new?chat_mode=${data.chat_mode}&model_name=${data.model}`, data);
};
export const addUser = (data: UserParam) => {
return POST<UserParam, UserParamResponse>('/api/v1/user/add', data);
};
/** Database Page */
@@ -90,15 +104,19 @@ export const postChatModeParamsFileLoad = ({
data,
config,
model,
userName,
sysCode,
}: {
convUid: string;
chatMode: string;
data: FormData;
model: string;
userName?: string;
sysCode?: string;
config?: Omit<AxiosRequestConfig, 'headers'>;
}) => {
return POST<FormData, ChatHistoryResponse>(
`/api/v1/chat/mode/params/file/load?conv_uid=${convUid}&chat_mode=${chatMode}&model_name=${model}`,
return POST<FormData, any>(
`/api/v1/resource/file/upload?conv_uid=${convUid}&chat_mode=${chatMode}&model_name=${model}&user_name=${userName}&sys_code=${sysCode}`,
data,
{
headers: {
@@ -109,6 +127,10 @@ export const postChatModeParamsFileLoad = ({
);
};
export const clearChatHistory = (conUid: string) => {
return POST<null, Record<string, string>>(`/api/v1/chat/dialogue/clear?con_uid=${conUid}`);
};
/** Menu */
export const delDialogue = (conv_uid: string) => {
return POST(`/api/v1/chat/dialogue/delete?con_uid=${conv_uid}`);
@@ -139,8 +161,8 @@ export const saveArguments = (knowledgeName: string, data: ArgumentsParams) => {
return POST<ArgumentsParams, IArguments>(`/knowledge/${knowledgeName}/argument/save`, data);
};
export const getSpaceList = () => {
return POST<any, Array<ISpace>>('/knowledge/space/list', {});
export const getSpaceList = (data: any) => {
return POST<any, Array<ISpace>>('/knowledge/space/list', data);
};
export const getDocumentList = (spaceName: string, data: Record<string, number | Array<number>>) => {
return POST<Record<string, number | Array<number>>, IDocumentResponse>(`/knowledge/${spaceName}/document/list`, data);
@@ -154,7 +176,7 @@ export const addDocument = (knowledgeName: string, data: DocumentParams) => {
};
export const addSpace = (data: AddKnowledgeParams) => {
return POST<AddKnowledgeParams, Array<any>>(`/knowledge/space/add`, data);
return POST<AddKnowledgeParams, number>(`/knowledge/space/add`, data);
};
export const getChunkStrategies = () => {
@@ -178,7 +200,7 @@ export const getChunkList = (spaceName: string, data: ChunkListParams) => {
};
export const delDocument = (spaceName: string, data: Record<string, number>) => {
return POST<Record<string, number>>(`/knowledge/${spaceName}/document/delete`, data);
return POST<Record<string, number>, null>(`/knowledge/${spaceName}/document/delete`, data);
};
export const delSpace = (data: Record<string, string>) => {
@@ -248,27 +270,12 @@ export const postChatFeedBackForm = ({ data, config }: { data: ChatFeedBackSchem
};
/** prompt */
export const getPromptList = (data: PromptParams) => {
return POST<PromptParams, Array<IPrompt>>('/prompt/list', data);
};
export const updatePrompt = (data: UpdatePromptParams) => {
return POST<UpdatePromptParams, []>('/prompt/update', data);
};
export const addPrompt = (data: UpdatePromptParams) => {
return POST<UpdatePromptParams, []>('/prompt/add', data);
};
/** AWEL Flow */
export const addFlow = (data: IFlowUpdateParam) => {
return POST<IFlowUpdateParam, IFlow>('/api/v1/serve/awel/flows', data);
};
export const getFlows = () => {
return GET<null, IFlowResponse>('/api/v1/serve/awel/flows');
};
export const getFlowById = (id: string) => {
return GET<null, IFlow>(`/api/v1/serve/awel/flows/${id}`);
};
@@ -286,13 +293,6 @@ export const getFlowNodes = () => {
};
/** app */
export const addApp = (data: IApp) => {
return POST<IApp, []>('/api/v1/app/create', data);
};
export const getAppList = (data: Record<string, string>) => {
return POST<Record<string, string>, IAppData>('/api/v1/app/list', data);
};
export const collectApp = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/app/collect', data);
@@ -302,36 +302,51 @@ export const unCollectApp = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/app/uncollect', data);
};
export const delApp = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/app/remove', data);
};
export const getAgents = () => {
return GET<object, IAgent[]>('/api/v1/agents/list', {});
};
export const getTeamMode = () => {
return GET<null, string[]>('/api/v1/team-mode/list');
};
export const getResourceType = () => {
return GET<null, string[]>('/api/v1/resource-type/list');
};
export const getResource = (data: Record<string, string>) => {
return GET<Record<string, string>, []>(`/api/v1/app/resources/list?type=${data.type}`);
export const publishApp = (app_code: string) => {
return POST<Record<string, any>, []>('/api/v1/app/publish', { app_code });
};
export const updateApp = (data: IApp) => {
return POST<IApp, []>('/api/v1/app/edit', data);
export const unPublishApp = (app_code: string) => {
return POST<Record<string, any>, []>('/api/v1/app/unpublish', { app_code });
};
export const addOmcDB = (params: Record<string, string>) => {
return POST<Record<string, any>, []>('/api/v1/chat/db/add', params);
};
export const getAppStrategy = () => {
return GET<null, []>(`/api/v1/llm-strategy/list`);
export const getAppInfo = (data: GetAppInfoParams) => {
return GET<GetAppInfoParams, IApp>('/api/v1/app/info', data);
};
export const getAppStrategyValues = (type: string) => {
return GET<string, []>(`/api/v1/llm-strategy/value/list?type=${type}`);
export const getSupportDBList = (db_name = '') => {
return GET<null, Record<string, any>>(`/api/v1/permission/db/list?db_name=${db_name}`);
};
export const recommendApps = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/app/hot/list', data);
};
export const flowSearch = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/serve/awel/flows', data);
};
export const modelSearch = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/controller/models', data);
};
export const getKnowledgeAdmins = (spaceId: string) => {
return GET<string, Record<string, any>>(`/knowledge/users/list?space_id=${spaceId}`);
};
export const updateKnowledgeAdmins = (data: Record<string, string>) => {
return POST<Record<string, any>, any[]>(`/knowledge/users/update`, data);
};
/** AWEL Flow */
/** app */
export const delApp = (data: Record<string, string>) => {
return POST<Record<string, string>, []>('/api/v1/app/remove', data);
};
export const getSpaceConfig = () => {

View File

@@ -6,6 +6,8 @@ import { useCallback, useMemo, useState } from 'react';
import MyEmpty from '../common/MyEmpty';
import { ClearOutlined, DownloadOutlined, GithubOutlined, LoadingOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import BlurredCard, { ChatButton, InnerDropdown } from '@/ant-components/common/blurredCard';
import moment from 'moment';
function MarketPlugins() {
const { t } = useTranslation();
@@ -96,6 +98,7 @@ function MarketPlugins() {
},
[actionIndex, pluginAction],
);
console.log(agents);
return (
<Spin spinning={loading}>
@@ -114,8 +117,7 @@ function MarketPlugins() {
</Form>
{!agents.length && !loading && <MyEmpty error={isError} refresh={refresh} />}
<div className="flex flex-wrap gap-2 md:gap-4">
{agents.map((agent, index) => (
<Card
{/* <Card
className="w-full md:w-1/2 lg:w-1/3 xl:w-1/4"
key={agent.id}
actions={[
@@ -142,7 +144,50 @@ function MarketPlugins() {
<Tooltip title={agent.description}>
<p className="mt-2 line-clamp-2 text-gray-400 text-sm">{agent.description}</p>
</Tooltip>
</Card>
</Card> */}
{agents.map((agent, index) => (
<BlurredCard
onClick={() => {
window.open(agent.storage_url, '_blank');
}}
description={agent.description}
name={agent.name}
key={agent.id}
Tags={
<div>
{agent.author && <Tag>{agent.author}</Tag>}
{agent.version && <Tag>v{agent.version}</Tag>}
{agent.type && <Tag>Type {agent.type}</Tag>}
{agent.storage_channel && <Tag>{agent.storage_channel}</Tag>}
</div>
}
LeftBottom={
<div className="flex gap-2">
<span>{agent.author}</span>
<span></span>
{agent?.gmt_created && <span>{moment(agent?.gmt_created).fromNow() + ' ' + t('update')}</span>}
</div>
}
RightBottom={
agent.installed ? (
<ChatButton
Icon={<ClearOutlined />}
text="Uninstall"
onClick={() => {
pluginAction(agent.name, index, false);
}}
/>
) : (
<ChatButton
Icon={<DownloadOutlined />}
text="Install"
onClick={() => {
pluginAction(agent.name, index, true);
}}
/>
)
}
/>
))}
</div>
</Spin>

View File

@@ -40,7 +40,7 @@ export default function AgentPanel(props: IProps) {
const getStrategy = async () => {
const [_, data] = await apiInterceptors(getAppStrategy());
if (data) {
setStrategyOptions(data?.map((item) => ({ label: item, value: item })));
setStrategyOptions(data?.map((item) => ({ label: item.name_cn, value: item.value })));
}
};

View File

@@ -1,28 +1,30 @@
import React, { useContext, useEffect, useState } from 'react';
import { Modal } from 'antd';
import { apiInterceptors, collectApp, delApp, newDialogue, unCollectApp } from '@/client/api';
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, collectApp, delApp, newDialogue, publishApp, unCollectApp, unPublishApp } from '@/client/api';
import { IApp } from '@/types/app';
import { DeleteFilled, MessageFilled, StarFilled, WarningOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { Modal, Popconfirm, Tooltip, message } from 'antd';
import { useRouter } from 'next/router';
import { ChatContext } from '@/app/chat-context';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import IconFont from '@/ant-components/common/Icon';
import { useRequest } from 'ahooks';
import GPTCard from '../common/gpt-card';
interface IProps {
updateApps: (data?: { is_collected: boolean }) => void;
updateApps: (params?: Record<string, any>) => void;
app: IApp;
handleEdit: (app: any) => void;
isCollected: boolean;
activeKey: string;
}
const { confirm } = Modal;
export default function AppCard(props: IProps) {
const { updateApps, app, handleEdit, isCollected } = props;
const { updateApps, app, handleEdit, activeKey } = props;
const { model } = useContext(ChatContext);
const router = useRouter();
const [isCollect, setIsCollect] = useState<string>(app.is_collected);
const { setAgent: setAgentToChat } = useContext(ChatContext);
const { t } = useTranslation();
@@ -42,30 +44,135 @@ export default function AppCard(props: IProps) {
cancelText: 'No',
async onOk() {
await apiInterceptors(delApp({ app_code: app.app_code }));
updateApps(isCollected ? { is_collected: isCollected } : undefined);
if (activeKey === 'collected') {
updateApps({ is_collected: 'true', ignore_user: 'true' });
} else {
updateApps();
}
},
});
};
useEffect(() => {
setIsCollect(app.is_collected);
}, [app]);
const collect = async () => {
const [error] = await apiInterceptors(isCollect === 'true' ? unCollectApp({ app_code: app.app_code }) : collectApp({ app_code: app.app_code }));
const [error] = await apiInterceptors(
app.is_collected === 'true' ? unCollectApp({ app_code: app.app_code }) : collectApp({ app_code: app.app_code }),
);
if (error) return;
updateApps(isCollected ? { is_collected: isCollected } : undefined);
setIsCollect(isCollect === 'true' ? 'false' : 'true');
if (activeKey === 'collected') {
updateApps({ is_collected: 'true', ignore_user: 'true' });
} else if (activeKey === 'common') {
updateApps({ ignore_user: 'true', published: 'true' });
} else {
updateApps();
}
};
const handleChat = async () => {
setAgentToChat?.(app.app_code);
const [, res] = await apiInterceptors(newDialogue({ chat_mode: 'chat_agent' }));
if (res) {
router.push(`/chat/?scene=chat_agent&id=${res.conv_uid}${model ? `&model=${model}` : ''}`);
// 原生应用跳转
if (app.team_mode === 'native_app') {
const { chat_scene = '' } = app.team_context;
router.push(`/chat?scene=${chat_scene}&id=${res.conv_uid}${model ? `&model=${model}` : ''}`);
} else {
setAgentToChat?.(app.app_code);
router.push(`/chat/?scene=chat_agent&id=${res.conv_uid}${model ? `&model=${model}` : ''}`);
}
}
};
// 发布或取消发布应用
const { run: operate } = useRequest(
async () => {
if (app.published === 'true') {
return await apiInterceptors(unPublishApp(app.app_code));
} else {
return await apiInterceptors(publishApp(app.app_code));
}
},
{
manual: true,
onSuccess: (data) => {
if (data[2]?.success) {
if (app.published === 'true') {
message.success(t('cancel_success'));
} else {
message.success(t('published_success'));
}
}
updateApps?.();
},
},
);
const publicContent = () => {
const { published = '' } = app;
const stopPropagationFn = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
return (
<Popconfirm
title={t('Tips')}
description={t(published == 'true' ? 'unPublish_desc' : 'publish_desc')}
onCancel={(e: any) => {
stopPropagationFn(e);
}}
onConfirm={async (e: any) => {
stopPropagationFn(e);
operate();
}}
>
<Tooltip title={t(published == 'true' ? 'unPublish' : 'publish')}>
{published == 'true' ? (
<IconFont
type="icon-unPublish-cloud"
style={{
fontSize: 20,
}}
onClick={stopPropagationFn}
/>
) : (
<IconFont
type="icon-publish-cloud"
style={{
fontSize: 20,
}}
onClick={stopPropagationFn}
/>
)}
</Tooltip>
</Popconfirm>
);
};
const canDelete = useMemo(() => {
return activeKey === 'app';
}, [activeKey]);
const operations = useMemo(() => {
const defaultArr = [
{
label: t('Chat'),
children: <MessageFilled />,
onClick: handleChat,
},
{
label: t('collect'),
children: <StarFilled className={app.is_collected === 'false' ? 'text-gray-400' : 'text-yellow-400'} />,
onClick: collect,
},
];
if (canDelete) {
defaultArr.push({
label: t('Delete'),
children: <DeleteFilled />,
onClick: () => showDeleteConfirm() as any,
});
}
return defaultArr;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app, canDelete]);
return (
<GPTCard
title={app.app_name}
@@ -77,27 +184,13 @@ export default function AppCard(props: IProps) {
{ text: app.team_mode, color: 'default' },
]}
onClick={() => {
if (!canDelete) {
return;
}
handleEdit(app);
}}
operations={[
{
label: t('Chat'),
children: <MessageFilled />,
onClick: handleChat,
},
{
label: t('collect'),
children: <StarFilled className={app.is_collected === 'false' ? 'text-gray-400' : 'text-yellow-400'} />,
onClick: collect,
},
{
label: t('Delete'),
children: <DeleteFilled />,
onClick: () => {
showDeleteConfirm();
},
},
]}
operations={operations}
extraContent={canDelete && publicContent()}
/>
);
}

View File

@@ -1,4 +1,4 @@
import { AgentParams, IAgent as IAgentParams, IApp, IDetail } from '@/types/app';
import { AgentParams, IAgent as IAgentParams, CreateAppParams, IDetail } from '@/types/app';
import { Dropdown, Form, Input, Modal, Select, Space, Spin, Tabs } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -60,7 +60,7 @@ export default function AppModal(props: IProps) {
setActiveKey(newActiveKey);
};
const createApp = async (app: IApp) => {
const createApp = async (app: CreateAppParams) => {
await apiInterceptors(type === 'add' ? addApp(app) : updateApp(app));
await updateApps();
};
@@ -302,7 +302,7 @@ export default function AppModal(props: IProps) {
<div>
<Modal
okText={t('Submit')}
title={type === 'edit' ? t('edit_application') : t('add_application')}
title={type === 'edit' ? 'edit application' : 'add application'}
open={open}
width={'65%'}
onCancel={handleCancel}
@@ -327,7 +327,7 @@ export default function AppModal(props: IProps) {
autoComplete="off"
onFinish={handleSubmit}
>
<Form.Item<FieldType> label={t('app_name')} name="app_name" rules={[{ required: true, message: t('Please_input_the_name') }]}>
<Form.Item<FieldType> label={'App Name'} name="app_name" rules={[{ required: true, message: t('Please_input_the_name') }]}>
<Input placeholder={t('Please_input_the_name')} />
</Form.Item>
<Form.Item<FieldType>
@@ -353,7 +353,7 @@ export default function AppModal(props: IProps) {
</div>
{curTeamModal !== 'awel_layout' ? (
<>
<div className="mb-5">{t('Agents')}</div>
<div className="mb-5">Agents</div>
<Tabs addIcon={renderAddIcon()} type="editable-card" onChange={onChange} activeKey={activeKey} onEdit={onEdit} items={agents} />
</>
) : (

View File

@@ -31,7 +31,7 @@ export default function ResourceCard(props: IProps) {
if (data) {
setResourceValueOptions(
data?.map((item) => {
return { label: item, value: item };
return { label: item.label, value: item.key + '' };
}),
);
} else {

View File

@@ -73,10 +73,10 @@ export const getVisAdvices = (props: { data: Datum[]; myChartAdvisor: Advisor; d
*/
const customDataProps = dataMetaMap
? Object.keys(dataMetaMap).map((item) => {
return { name: item, ...dataMetaMap[item] };
})
return { name: item, ...dataMetaMap[item] };
})
: null;
// 可根据需要选择是否使用全部 fields 进行推荐
const useAllFields = false;
// 挑选出维值不只有一个的字段
@@ -84,11 +84,11 @@ export const getVisAdvices = (props: { data: Datum[]; myChartAdvisor: Advisor; d
const selectedFields =
size(allFieldsInfo) > 2
? allFieldsInfo?.filter((field) => {
if (field.recommendation === 'string' || field.recommendation === 'date') {
return field.distinct && field.distinct > 1;
}
return true;
})
if (field.recommendation === 'string' || field.recommendation === 'date') {
return field.distinct && field.distinct > 1;
}
return true;
})
: allFieldsInfo;
const allAdvices = myChartAdvisor?.adviseWithLog({

View File

@@ -1,5 +1,5 @@
import { hasSubset } from '../advisor/utils';
import { findOrdinalField, processDateEncode, findNominalField, getLineSize, sortData } from './util';
import { processDateEncode, findOrdinalField, findNominalField, getLineSize, sortData } from './util';
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
import type { Datum } from '@antv/ava';

View File

@@ -1,5 +1,5 @@
import { hasSubset } from '../advisor/utils';
import { findNominalField, findOrdinalField, getLineSize, processDateEncode, sortData } from './util';
import { processDateEncode, findNominalField, findOrdinalField, getLineSize, sortData } from './util';
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
import { Datum } from '@antv/ava';

View File

@@ -1,5 +1,5 @@
import type { Datum, FieldInfo } from "@antv/ava";
import { hasSubset, intersects } from "../advisor/utils";
import { hasSubset, intersects } from '../advisor/utils';
import { cloneDeep, uniq } from "lodash";
/**

View File

@@ -3,7 +3,7 @@ import { Advice, Advisor, Datum } from '@antv/ava';
import { Chart, ChartRef } from '@berryv/g2-react';
import i18n, { I18nKeys } from '@/app/i18n';
import { customizeAdvisor, getVisAdvices } from './advisor/pipeline';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useContext, useEffect, useMemo, useState, useRef } from 'react';
import { defaultAdvicesFilter } from './advisor/utils';
import { AutoChartProps, ChartType, CustomAdvisorConfig, CustomChart, Specification } from './types';
import { customCharts } from './charts';
@@ -16,8 +16,7 @@ import { DownloadOutlined } from '@ant-design/icons';
const { Option } = Select;
export const AutoChart = (props: AutoChartProps) => {
const { chartType, scopeOfCharts, ruleConfig, data: originalData } = props;
const { data: originalData, chartType, scopeOfCharts, ruleConfig } = props;
// 处理空值数据 (为'-'的数据)
const data = processNilData(originalData) as Datum[];
const { mode } = useContext(ChatContext);
@@ -99,7 +98,7 @@ export const AutoChart = (props: AutoChartProps) => {
// 处理 ava 内置折线图的排序问题
const dataAnalyzerOutput = advisor?.dataAnalyzer.execute({ data })
if (dataAnalyzerOutput && 'dataProps' in dataAnalyzerOutput) {
spec.data = sortData({ data: spec.data, xField: dataAnalyzerOutput.dataProps?.find(field => field.recommendation === 'date'), chartType: chartTypeInput });
spec.data = sortData({ data: spec.data, xField: dataAnalyzerOutput.dataProps?.find((field: any) => field.recommendation === 'date'), chartType: chartTypeInput });
}
}
if (chartTypeInput === 'pie_chart' && spec?.encode?.color) {
@@ -111,8 +110,8 @@ export const AutoChart = (props: AutoChartProps) => {
key={chartTypeInput}
options={{
...spec,
theme: mode,
autoFit: true,
theme: mode,
height: 300,
}}
ref={chartRef}
@@ -120,7 +119,7 @@ export const AutoChart = (props: AutoChartProps) => {
);
}
}
}, [advices, renderChartType]);
}, [advices, mode, renderChartType]);
if (renderChartType) {
return (
@@ -159,7 +158,7 @@ export const AutoChart = (props: AutoChartProps) => {
</Tooltip>
</Col>
</Row>
<div className="auto-chart-content">{visComponent}</div>
<div className="flex">{visComponent}</div>
</div>
);
}

View File

@@ -10,6 +10,7 @@ type Props = {
};
function Chart({ chartsData }: Props) {
console.log(chartsData,'xxx')
const chartRows = useMemo(() => {
if (chartsData) {
let res = [];
@@ -66,7 +67,7 @@ function Chart({ chartsData }: Props) {
return <LineChart key={chart.chart_uid} chart={chart} />;
} else if (chart.chart_type === 'BarChart' || chart.type === 'BarChart') {
return <BarChart key={chart.chart_uid} chart={chart} />;
} else if (chart.chart_type === 'Table' || chart.type === 'Table') {
} else if (chart.chart_type === 'Table' || chart.type === 'TableChartData') {
return <TableChart key={chart.chart_uid} chart={chart} />;
}
})}

View File

@@ -11,7 +11,7 @@ interface Props {
}
function formatMarkdownVal(val: string) {
return val.replace(/<table(\w*=[^>]+)>/gi, '<table $1>').replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
return val?.replace(/<table(\w*=[^>]+)>/gi, '<table $1>').replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
}
function AgentContent({ content }: Props) {

View File

@@ -16,7 +16,7 @@ import MyEmpty from '../common/MyEmpty';
const ChatContainer = () => {
const searchParams = useSearchParams();
const { scene, chatId, model, agent, setModel, history, setHistory } = useContext(ChatContext);
const chat = useChat({});
const { chat } = useChat({});
const initMessage = (searchParams && searchParams.get('initMessage')) ?? '';
const [loading, setLoading] = useState<boolean>(false);
@@ -34,9 +34,11 @@ const ChatContainer = () => {
if (contextTemp) {
try {
const contextObj = typeof contextTemp === 'string' ? JSON.parse(contextTemp) : contextTemp;
console.log('contextObj', contextObj);
setChartsData(contextObj?.template_name === 'report' ? contextObj?.charts : undefined);
} catch (e) {
setChartsData(undefined);
console.log(e);
setChartsData([]);
}
}
};
@@ -117,7 +119,9 @@ const ChatContainer = () => {
<Chart chartsData={chartsData} />
</div>
)}
{!chartsData?.length && scene === 'chat_dashboard' && <MyEmpty className="w-full xl:w-3/4 h-1/2 xl:h-full" />}
{!chartsData?.length && scene === 'chat_dashboard' && (
<MyEmpty className="w-full xl:w-3/4 h-1/2 xl:h-full" />
)}
{/** chat panel */}
<div
className={classNames('flex flex-1 flex-col overflow-hidden', {

View File

@@ -1,7 +1,11 @@
import ModelIcon from '@/ant-components/chat/content/ModelIcon';
import { LinkOutlined, SwapRightOutlined } from '@ant-design/icons';
import { Popover, Space } from 'antd';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import markdownComponents from './config';
import { renderModelIcon } from '../header/model-selector';
import { SwapRightOutlined } from '@ant-design/icons';
import ReferencesContent from './ReferencesContent';
interface Props {
data: {
@@ -9,27 +13,30 @@ interface Props {
receiver: string;
model: string | null;
markdown: string;
resource: any;
}[];
}
function AgentMessages({ data }: Props) {
if (!data || !data.length) return null;
return (
<>
{data.map((item, index) => (
<div key={index} className="rounded my-4 md:my-6">
<div key={index} className="rounded">
<div className="flex items-center mb-3 text-sm">
{item.model ? renderModelIcon(item.model) : <div className="rounded-full w-6 h-6 bg-gray-100" />}
{item.model ? <ModelIcon model={item.model} /> : <div className="rounded-full w-6 h-6 bg-gray-100" />}
<div className="ml-2 opacity-70">
{item.sender}
<SwapRightOutlined className="mx-2 text-base" />
{item.receiver}
</div>
</div>
<div className="whitespace-normal text-sm">
<ReactMarkdown components={markdownComponents}>{item.markdown}</ReactMarkdown>
<div className="whitespace-normal text-sm mb-3">
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{item.markdown}
</ReactMarkdown>
</div>
{item.resource && item.resource !== 'null' && <ReferencesContent references={item.resource} />}
</div>
))}
</>

View File

@@ -1,6 +1,9 @@
import { CaretRightOutlined, CheckOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { Collapse } from 'antd';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import markdownComponents from './config';
interface Props {
@@ -25,7 +28,7 @@ function AgentPlans({ data }: Props) {
return {
key: index,
label: (
<div className="whitespace-normal">
<div>
<span>
{item.name} - {item.agent}
</span>
@@ -36,7 +39,11 @@ function AgentPlans({ data }: Props) {
)}
</div>
),
children: <ReactMarkdown components={markdownComponents}>{item.markdown}</ReactMarkdown>,
children: (
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
{item.markdown}
</ReactMarkdown>
),
};
})}
/>

View File

@@ -20,11 +20,10 @@ function ChartView({ data, type, sql }: { data: Datum[]; type: BackEndChartType;
label: 'Chart',
children: <AutoChart data={data} chartType={getChartType(type)} />,
};
const SqlItem = {
key: 'sql',
label: 'SQL',
children: <CodePreview language="sql" code={formatSql(sql)} />,
children: <CodePreview language="sql" code={formatSql(sql ?? '', 'mysql') as string} />,
};
const DataItem = {
key: 'data',

View File

@@ -25,7 +25,7 @@ export function CodePreview({ code, light, dark, language, customStyle }: Props)
icon={<CopyOutlined />}
onClick={() => {
const success = copy(code);
message[success ? 'success' : 'error'](success ? 'Copy success' : 'Copy failed');
message[success ? 'success' : 'error'](success ? '复制成功' : '复制失败');
}}
/>
<SyntaxHighlighter customStyle={customStyle} language={language} style={mode === 'dark' ? dark ?? coldarkDark : light ?? oneDark}>

View File

@@ -1,19 +1,23 @@
import { LinkOutlined, ReadOutlined, SyncOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import { Table, Image, Tag, Tabs, TabsProps, Popover } from 'antd';
import { Reference } from '@/types/chat';
import { AutoChart, BackEndChartType, getChartType } from '@/components/chart';
import { CodePreview } from './code-preview';
import { LinkOutlined, ReadOutlined, SyncOutlined } from '@ant-design/icons';
import { Datum } from '@antv/ava';
import { Image, Table, Tabs, TabsProps, Tag } from 'antd';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import { IChunk } from '@/types/knowledge';
import AgentPlans from './agent-plans';
import remarkGfm from 'remark-gfm';
import AgentMessages from './agent-messages';
import VisConvertError from './vis-convert-error';
import AgentPlans from './agent-plans';
import { CodePreview } from './code-preview';
import ReferencesContent from './ReferencesContent';
import VisChart from './vis-chart';
import VisCode from './vis-code';
import VisConvertError from './vis-convert-error';
import VisDashboard from './vis-dashboard';
import VisPlugin from './vis-plugin';
import VisCode from './vis-code';
import VisAppLink from './VisAppLink';
import VisChatLink from './VisChatLink';
import VisResponse from './VisResponse';
import { formatSql } from '@/utils';
type MarkdownComponent = Parameters<typeof ReactMarkdown>['0']['components'];
@@ -106,6 +110,22 @@ const basicComponents: MarkdownComponent = {
}
}
if (lang === 'vis-app-link') {
try {
const data = JSON.parse(content) as Parameters<typeof VisAppLink>[0]['data'];
return <VisAppLink data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-api-response') {
try {
const data = JSON.parse(content) as Parameters<typeof VisResponse>[0]['data'];
return <VisResponse data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
return (
<>
{!inline ? (
@@ -115,7 +135,7 @@ const basicComponents: MarkdownComponent = {
{children}
</code>
)}
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]}>
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
{matchValues.join('\n')}
</ReactMarkdown>
</>
@@ -131,9 +151,7 @@ const basicComponents: MarkdownComponent = {
return <li className={`text-sm leading-7 ml-5 pl-2 text-gray-600 dark:text-gray-300 ${ordered ? 'list-decimal' : 'list-disc'}`}>{children}</li>;
},
table({ children }) {
return (
<table className="my-2 rounded-tl-md rounded-tr-md max-w-full bg-white dark:bg-gray-800 text-sm rounded-lg overflow-hidden">{children}</table>
);
return <table className="my-2 rounded-tl-md rounded-tr-md bg-white dark:bg-gray-800 text-sm rounded-lg overflow-hidden">{children}</table>;
},
thead({ children }) {
return <thead className="bg-[#fafafa] dark:bg-black font-semibold">{children}</thead>;
@@ -178,7 +196,7 @@ const basicComponents: MarkdownComponent = {
Image Loading...
</Tag>
}
fallback="/images/fallback.png"
fallback="/pictures/fallback.png"
/>
</div>
);
@@ -190,6 +208,43 @@ const basicComponents: MarkdownComponent = {
</blockquote>
);
},
button({ children, className, ...restProps }) {
if (className === 'chat-link') {
const msg = (restProps as any)?.['data-msg'];
return <VisChatLink msg={msg}>{children}</VisChatLink>;
}
return (
<button className={className} {...restProps}>
{children}
</button>
);
},
};
const returnSqlVal = (val: string) => {
const punctuationMap: any = {
'': ',',
'。': '.',
'': '?',
'': '!',
'': ':',
'': ';',
'“': '"',
'”': '"',
'': "'",
'': "'",
'': '(',
'': ')',
'【': '[',
'】': ']',
'《': '<',
'》': '>',
'—': '-',
'、': ',',
'…': '...',
};
const regex = new RegExp(Object.keys(punctuationMap).join('|'), 'g');
return val.replace(regex, (match) => punctuationMap[match]);
};
const extraComponents: MarkdownComponent = {
@@ -228,12 +283,12 @@ const extraComponents: MarkdownComponent = {
const SqlItem = {
key: 'sql',
label: 'SQL',
children: <CodePreview code={formatSql(data?.sql, 'mysql')} language="sql" />,
children: <CodePreview code={formatSql(returnSqlVal(data?.sql), 'mysql') as string} language={'sql'} />,
};
const DataItem = {
key: 'data',
label: 'Data',
children: <Table dataSource={data?.data} columns={columns} scroll={{x:true}} />,
children: <Table dataSource={data?.data} columns={columns} scroll={{x:true}} virtual={true} />,
};
const TabItems: TabsProps['items'] = data?.type === 'response_table' ? [DataItem, SqlItem] : [ChartItem, SqlItem, DataItem];
@@ -245,71 +300,15 @@ const extraComponents: MarkdownComponent = {
);
},
references: function ({ title, references, children }) {
let referenceData;
// Low version compatibility, read data from children
if (children) {
try {
referenceData = JSON.parse(children as string);
title = referenceData.title;
references = referenceData.references;
const referenceData = JSON.parse(children as string);
const references = referenceData.references;
return <ReferencesContent references={references} />;
} catch (error) {
console.log('parse references failed', error);
return <p className="text-sm text-red-500">Render Reference Error!</p>;
}
} else {
// new version, read from tag props.
try {
references = JSON.parse(references as string);
} catch (error) {
console.log('parse references failed', error);
return <p className="text-sm text-red-500">Render Reference Error!</p>;
return null;
}
}
if (!references || references?.length < 1) {
return null;
}
return (
<div className="border-t-[1px] border-gray-300 mt-3 py-2">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
<LinkOutlined className="mr-2" />
<span className="font-semibold">{title}</span>
</p>
{references.map((reference: Reference, index: number) => (
<div key={`file_${index}`} className="text-sm font-normal block ml-2 h-6 leading-6 overflow-hidden">
<span className="inline-block w-6">[{index + 1}]</span>
<span className="mr-2 lg:mr-4 text-blue-400">{reference.name}</span>
{reference?.chunks?.map((chunk: IChunk | number, index) => (
<span key={`chunk_${index}`}>
{typeof chunk === 'object' ? (
<Popover
content={
<div className="max-w-4xl">
<p className="mt-2 font-bold mr-2 border-t border-gray-500 pt-2">Content:</p>
<p>{chunk?.content || 'No Content'}</p>
<p className="mt-2 font-bold mr-2 border-t border-gray-500 pt-2">MetaData:</p>
<p>{chunk?.meta_info || 'No MetaData'}</p>
<p className="mt-2 font-bold mr-2 border-t border-gray-500 pt-2">Score:</p>
<p>{chunk?.recall_score || ''}</p>
</div>
}
title="Chunk Information"
>
<span className="cursor-pointer text-blue-500 ml-2" key={`chunk_content_${chunk?.id}`}>
{chunk?.id}
</span>
</Popover>
) : (
<span className="cursor-pointer text-blue-500 ml-2" key={`chunk_id_${chunk}`}>
{chunk}
</span>
)}
{index < reference?.chunks.length - 1 && <span key={`chunk_comma_${index}`}>,</span>}
</span>
))}
</div>
))}
</div>
);
},
summary: function ({ children }) {
return (

View File

@@ -13,7 +13,10 @@ interface Props {
}
function VisChart({ data }: Props) {
return <ChartView data={data.data} type={data.type} sql={data.sql} />;
if (!data) {
return null;
}
return <ChartView data={data?.data} type={data?.type} sql={data?.sql} />;
}
export default VisChart;

View File

@@ -2,8 +2,10 @@ import { CheckOutlined, ClockCircleOutlined, CloseOutlined, LoadingOutlined } fr
import classNames from 'classnames';
import { ReactNode } from 'react';
import ReactMarkdown from 'react-markdown';
import markdownComponents from './config';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import markdownComponents from './config';
interface IVisPlugin {
name: string;
@@ -42,15 +44,16 @@ const pluginViewStatusMapper: Record<IVisPlugin['status'], { bgClass: string; ic
function VisPlugin({ data }: Props) {
const { bgClass, icon } = pluginViewStatusMapper[data.status] ?? {};
return (
<div className="bg-theme-light dark:bg-theme-dark-container rounded overflow-hidden my-2 flex flex-col lg:max-w-[80%]">
<div className="bg-theme-light dark:bg-theme-dark-container rounded overflow-hidden my-2 flex flex-col">
<div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}>
{data.name}
{icon}
</div>
{data.result ? (
<div className="px-4 md:px-6 py-4 text-sm whitespace-normal">
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]}>
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
{data.result ?? ''}
</ReactMarkdown>
</div>

View File

@@ -1,25 +1,25 @@
import { useState, useRef, useEffect, useMemo, useContext } from 'react';
import { useSearchParams } from 'next/navigation';
import MonacoEditor from './monaco-editor';
import ChatContent from './chat-content';
import ChatFeedback from './chat-feedback';
import { ChatContext } from '@/app/chat-context';
import { FeedBack, IChatDialogueMessageSchema } from '@/types/chat';
import classNames from 'classnames';
import { Modal, message, Tooltip } from 'antd';
import { renderModelIcon } from './header/model-selector';
import { cloneDeep } from 'lodash';
import copy from 'copy-to-clipboard';
import { useTranslation } from 'react-i18next';
import CompletionInput from '../common/completion-input';
import { useAsyncEffect } from 'ahooks';
import { STORAGE_INIT_MESSAGE_KET } from '@/utils';
import { Button, IconButton } from '@mui/joy';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
import { getInitMessage } from '@/utils';
import { apiInterceptors, getChatFeedBackSelect } from '@/client/api';
import useSummary from '@/hooks/use-summary';
import { FeedBack, IChatDialogueMessageSchema } from '@/types/chat';
import { STORAGE_INIT_MESSAGE_KET, getInitMessage } from '@/utils';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
import { Button, IconButton } from '@mui/joy';
import { useAsyncEffect } from 'ahooks';
import { Modal, Tooltip, message } from 'antd';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import { cloneDeep } from 'lodash';
import { useSearchParams } from 'next/navigation';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CompletionInput from '../common/completion-input';
import AgentContent from './agent-content';
import ChatContent from './chat-content';
import ChatFeedback from './chat-feedback';
import { renderModelIcon } from './header/model-selector';
import MonacoEditor from './monaco-editor';
import MyEmpty from '../common/MyEmpty';
type Props = {
@@ -93,12 +93,12 @@ const Completion = ({ messages, onSubmit }: Props) => {
const result = copy(pureStr);
if (result) {
if (pureStr) {
messageApi.open({ type: 'success', content: t('Copy_success') });
messageApi.open({ type: 'success', content: t('copy_success') });
} else {
messageApi.open({ type: 'warning', content: t('Copy_nothing') });
messageApi.open({ type: 'warning', content: t('copy_nothing') });
}
} else {
messageApi.open({ type: 'error', content: t('Copry_error') });
messageApi.open({ type: 'error', content: t('copy_failed') });
}
};
@@ -184,7 +184,7 @@ const Completion = ({ messages, onSubmit }: Props) => {
question={showMessages?.filter((e) => e?.role === 'human' && e?.order === content.order)[0]?.context}
knowledge_space={spaceNameOriginal || dbParam || ''}
/>
<Tooltip title={t('Copy')}>
<Tooltip title={t('copy')}>
<Button
onClick={() => onCopyContext(content?.context)}
slots={{ root: IconButton }}

View File

@@ -1,8 +1,8 @@
import React, { ChangeEvent, Key, useEffect, useMemo, useState } from 'react';
import { useRequest } from 'ahooks';
import { Button, Select, Table, Tooltip } from 'antd';
import { Input, Tree } from 'antd';
import Icon from '@ant-design/icons';
import { Input, Tree } from 'antd';
import type { DataNode } from 'antd/es/tree';
import MonacoEditor, { ISession } from './monaco-editor';
import { sendGetRequest, sendSpacePostRequest } from '@/utils/request';
@@ -10,6 +10,7 @@ import { useSearchParams } from 'next/navigation';
import { OnChange } from '@monaco-editor/react';
import Header from './header';
import Chart from '../chart';
import { CaretRightOutlined, LeftOutlined, RightOutlined, SaveFilled } from '@ant-design/icons';
import { ColumnType } from 'antd/es/table';
import Database from '../icons/database';
@@ -62,7 +63,6 @@ interface ITableTreeItem {
function DbEditorContent({ layout = 'LR', editorValue, chartData, tableData, tables, handleChange }: IProps) {
const chartWrapper = useMemo(() => {
if (!chartData) return null;
return (
<div className="flex-1 overflow-auto p-2" style={{ flexShrink: 0, overflow: 'hidden' }}>
<Chart chartsData={[chartData]} />
@@ -87,7 +87,6 @@ function DbEditorContent({ layout = 'LR', editorValue, chartData, tableData, tab
return {
columns: tbCols,
dataSource: tbDatas,
};
}, [tableData]);
const session: ISession = useMemo(() => {
@@ -103,13 +102,13 @@ function DbEditorContent({ layout = 'LR', editorValue, chartData, tableData, tab
})
});
return {
async getTableList(schemaName) {
async getTableList(schemaName: any) {
if (schemaName && schemaName!== db?.title) {
return [];
}
return tableList?.map((table: ITableTreeItem) => table.title) || [];
},
async getTableColumns(tableName) {
async getTableColumns(tableName: any) {
return map[tableName] || [];
},
async getSchemaList() {
@@ -117,6 +116,7 @@ function DbEditorContent({ layout = 'LR', editorValue, chartData, tableData, tab
}
};
}, [tables])
return (
<div
className={classNames('flex w-full flex-1 h-full gap-2 overflow-hidden', {
@@ -146,7 +146,7 @@ function DbEditor() {
const [searchValue, setSearchValue] = useState('');
const [currentRound, setCurrentRound] = useState<null | string | number>();
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [chartData, setChartData] = useState();
const [chartData, setChartData] = useState<any>();
const [editorValue, setEditorValue] = useState<EditorValueProps | EditorValueProps[]>();
const [newEditorValue, setNewEditorValue] = useState<EditorValueProps>();
const [tableData, setTableData] = useState<{ columns: string[]; values: (string | number)[] }>();
@@ -175,7 +175,7 @@ function DbEditor() {
const { run: runSql, loading: runLoading } = useRequest(
async () => {
const db_name = rounds?.data?.find((item) => item.round === currentRound)?.db_name;
const db_name = rounds?.data?.find((item: any) => item.round === currentRound)?.db_name;
return await sendSpacePostRequest(`/api/v1/editor/sql/run`, {
db_name,
sql: newEditorValue?.sql,
@@ -194,7 +194,7 @@ function DbEditor() {
const { run: runCharts, loading: runChartsLoading } = useRequest(
async () => {
const db_name = rounds?.data?.find((item) => item.round === currentRound)?.db_name;
const db_name = rounds?.data?.find((item: any) => item.round === currentRound)?.db_name;
const params: {
db_name: string;
sql?: string;
@@ -257,7 +257,7 @@ function DbEditor() {
const { run: submitChart, loading: submitChartLoading } = useRequest(
async () => {
const db_name = rounds?.data?.find((item) => item.round === currentRound)?.db_name;
const db_name = rounds?.data?.find((item: any) => item.round === currentRound)?.db_name;
return await sendSpacePostRequest(`/api/v1/chart/editor/submit`, {
conv_uid: id,
chart_title: newEditorValue?.title,
@@ -365,7 +365,6 @@ function DbEditor() {
</div>
</Tooltip>
);
if (item.children) {
const itemKey = parentKey ? String(parentKey) + '_' + item.key : item.key;
return { title: strTitle, showTitle, key: itemKey, children: loop(item.children, itemKey) };
@@ -392,7 +391,6 @@ function DbEditor() {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key, title } = node;
res.push({ key, title: title as string, parentKey });
if (node.children) {
generateList(node.children, key);
@@ -477,8 +475,7 @@ function DbEditor() {
<div className="flex flex-col w-full h-full overflow-hidden">
<Header />
<div className="relative flex flex-1 p-4 pt-0 overflow-hidden">
{/* Database Tree Node */}
<div className="group/side relative mr-4">
<div className="relative flex overflow-hidden mr-4">
<div
className={classNames('h-full relative transition-[width] overflow-hidden', {
'w-0': isMenuExpand,
@@ -621,7 +618,6 @@ function DbEditor() {
})}
>
<DbEditorContent
layout={layout}
editorValue={item}
handleChange={(value) => {
const { sql, thoughts } = resolveSqlAndThoughts(value);

View File

@@ -101,7 +101,7 @@ function ExcelUpload({ convUid, chatMode, onComplete, ...props }: PropsWithChild
{loading ? (percent === 100 ? 'Analysis' : 'Uploading') : 'Upload'}
</Button>
{!!fileList.length && (
<div className="mt-2 text-gray-500 text-sm flex items-center">
<div className="mt-2 text-gray-500 text-sm flex items-center" onClick={() => setFileList([])}>
<LinkOutlined className="mr-2" />
<span>{fileList[0]?.name}</span>
</div>

View File

@@ -3,8 +3,8 @@
*/
import { ChatContext } from '@/app/chat-context';
import { Select } from 'antd';
import { MODEL_ICON_MAP } from '@/utils/constants';
import { Select } from 'antd';
import Image from 'next/image';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
@@ -26,6 +26,7 @@ export function renderModelIcon(model?: string, props?: { width: number; height:
width={width || 24}
height={height || 24}
src={MODEL_ICON_MAP[model]?.icon || DEFAULT_ICON_URL}
key={MODEL_ICON_MAP[model]?.icon || DEFAULT_ICON_URL}
alt="llm"
/>
);

View File

@@ -2,10 +2,10 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
import Editor, { OnChange, loader } from '@monaco-editor/react';
import classNames from 'classnames';
import { useContext, useMemo } from 'react';
import { ChatContext } from '@/app/chat-context';
import { formatSql } from '@/utils';
import { getModelService } from './ob-editor/service';
import { useLatest } from 'ahooks';
import { ChatContext } from '@/app/chat-context';
import { github, githubDark } from './ob-editor/theme';
import { register } from './ob-editor/ob-plugin';
@@ -24,7 +24,6 @@ interface MonacoEditorProps {
onChange?: OnChange;
thoughts?: string;
session?: ISession;
}
let plugin = null;
@@ -33,6 +32,7 @@ monaco.editor.defineTheme('githubDark', githubDark as any);
export default function MonacoEditor({ className, value, language = 'mysql', onChange, thoughts, session }: MonacoEditorProps) {
// merge value and thoughts
const editorValue = useMemo(() => {
if (language !== 'mysql') {
return value;
@@ -58,7 +58,6 @@ export default function MonacoEditor({ className, value, language = 'mysql', onC
)
}
return (
<Editor
className={classNames(className)}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ interface Props {
}
function ChatDialog({ title, chatMode, completionApi, chatParams, model = '' }: Props) {
const chat = useChat({ queryAgentURL: completionApi });
const { chat } = useChat({ queryAgentURL: completionApi });
const [loading, setLoading] = useState(false);
const [list, setList] = useState<IChatDialogueMessageSchema[]>([]);

View File

@@ -1,22 +1,23 @@
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, getDocumentList } from '@/client/api';
import { IDocument } from '@/types/knowledge';
import { SendOutlined } from '@ant-design/icons';
import { Button, Input } from 'antd';
import { PropsWithChildren, useContext, useEffect, useMemo, useRef, useState } from 'react';
import PromptBot from './prompt-bot';
import DocUpload from '../chat/doc-upload';
import DocList from '../chat/doc-list';
import { IDocument } from '@/types/knowledge';
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, getDocumentList } from '@/client/api';
import DocUpload from '../chat/doc-upload';
import PromptBot from './prompt-bot';
type TextAreaProps = Omit<Parameters<typeof Input.TextArea>[0], 'value' | 'onPressEnter' | 'onChange' | 'onSubmit'>;
interface Props {
loading?: boolean;
onSubmit: (val: string) => void;
handleFinish?: (val: boolean) => void;
loading?: boolean;
placeholder?: string;
}
function CompletionInput({ children, loading, onSubmit, handleFinish, ...props }: PropsWithChildren<Props & TextAreaProps>) {
function CompletionInput({ children, loading, onSubmit, handleFinish, placeholder, ...props }: PropsWithChildren<Props & TextAreaProps>) {
const { dbParam, scene } = useContext(ChatContext);
const [userInput, setUserInput] = useState('');
@@ -77,6 +78,7 @@ function CompletionInput({ children, loading, onSubmit, handleFinish, ...props }
}
setUserInput(e.target.value);
}}
placeholder={placeholder}
/>
<Button
className="ml-2 flex items-center justify-center absolute right-0 bottom-0"

View File

@@ -1,5 +1,5 @@
import React, { HtmlHTMLAttributes, PropsWithChildren, ReactNode, memo, useCallback, useMemo } from 'react';
import { Tag, TagProps, Tooltip } from 'antd';
import { Popover, Tag, TagProps, Tooltip } from 'antd';
import classNames from 'classnames';
import Image from 'next/image';
@@ -25,6 +25,7 @@ interface Props {
icon?: ReactNode;
iconBorder?: boolean;
onClick?: () => void;
extraContent?: ReactNode;
}
function GPTCard({
@@ -37,6 +38,7 @@ function GPTCard({
disabled,
operations,
className,
extraContent,
...props
}: PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & Props>) {
const iconNode = useMemo(() => {
@@ -93,11 +95,14 @@ function GPTCard({
)}
{...props}
>
<div className="p-4">
<div className="p-4 0">
<div className="flex items-center">
{iconNode}
<div className="flex flex-col">
<h2 className="text-sm font-semibold">{title}</h2>
<Popover title={title}>
<h2 className="text-sm font-semibold line-clamp-1 pr-8">{title}</h2>
</Popover>
{tagNode}
</div>
</div>
@@ -128,6 +133,7 @@ function GPTCard({
</div>
)}
</div>
<div className="absolute top-2 right-4 ">{extraContent}</div>
</div>
);
}

View File

@@ -1,12 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Button, Form, Input, InputNumber, Modal, Select, message } from 'antd';
import { Button, Form, Input, InputNumber, Modal, Select, Spin, Tooltip, message } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import { apiInterceptors, postDbAdd, postDbEdit, postDbTestConnect } from '@/client/api';
import { addOmcDB, apiInterceptors, getSupportDBList, postDbAdd, postDbEdit, postDbTestConnect } from '@/client/api';
import { DBOption, DBType, DbListResponse, PostDbParams } from '@/types/db';
import { isFileDb } from '@/pages/database';
import { useTranslation } from 'react-i18next';
import { useDebounceFn } from 'ahooks';
type DBItem = DbListResponse[0];
type DBItem = DbListResponse[0] & { db_arn?: string };
interface Props {
dbTypeList: DBOption[];
@@ -23,7 +24,8 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
const { t } = useTranslation();
const [form] = Form.useForm<DBItem>();
const dbType = Form.useWatch('db_type', form);
const [omcDBList, setOmcDBList] = useState([]);
const [omcListLoading, setOmcListLoading] = useState(false);
const fileDb = useMemo(() => isFileDb(dbTypeList, dbType), [dbTypeList, dbType]);
useEffect(() => {
@@ -35,6 +37,9 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
useEffect(() => {
if (editValue) {
form.setFieldsValue({ ...editValue });
if (editValue.db_type === 'omc') {
form.setFieldValue('db_arn', editValue.db_path);
}
}
}, [editValue]);
@@ -45,7 +50,33 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
}, [open]);
const onFinish = async (val: DBItem) => {
const { db_host, db_path, db_port, ...params } = val;
const { db_host, db_path, db_port, db_type, ...params } = val;
setLoading(true);
if (db_type === 'omc') {
const item = omcDBList?.find((item: any) => item.arn === val.db_name) as any;
try {
const [err] = await apiInterceptors(
addOmcDB({
db_type: 'omc',
file_path: val.db_arn || '',
comment: val.comment,
db_name: item?.dbName || val.db_name,
}),
);
if (err) {
message.error(err.message);
return;
}
message.success('success');
onSuccess?.();
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
}
if (!editValue && dbNames.some((item) => item === params.db_name)) {
message.error('The database already exists!');
return;
@@ -53,10 +84,10 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
const data: PostDbParams = {
db_host: fileDb ? undefined : db_host,
db_port: fileDb ? undefined : db_port,
db_type: db_type,
file_path: fileDb ? db_path : undefined,
...params,
};
setLoading(true);
try {
const [testErr] = await apiInterceptors(postDbTestConnect(data));
if (testErr) return;
@@ -73,24 +104,78 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
setLoading(false);
}
};
console.log(form.getFieldValue('db_type'));
const { run: fetchOmcList } = useDebounceFn(
async (name: string) => {
setOmcListLoading(true);
const [error, data = []] = (await apiInterceptors(getSupportDBList(name))) as any;
setOmcListLoading(false);
setOmcDBList(data.map((item: any) => ({ ...item, label: item.dbName, value: item.arn })));
},
{
wait: 500,
},
);
console.log('omcDBList', omcDBList);
const lockDBType = useMemo(() => !!editValue || !!choiceDBType, [editValue, choiceDBType]);
return (
<Modal open={open} width={400} title={editValue ? t('Edit') : t('create_database')} maskClosable={false} footer={null} onCancel={onClose}>
<Form form={form} className="pt-2" labelCol={{ span: 6 }} labelAlign="left" onFinish={onFinish}>
<Form.Item name="db_type" label="DB Type" className="mb-3" rules={[{ required: true }]}>
<Select aria-readonly={lockDBType} disabled={lockDBType} options={dbTypeList} />
</Form.Item>
<Form.Item name="db_name" label="DB Name" className="mb-3" rules={[{ required: true }]}>
<Input readOnly={!!editValue} disabled={!!editValue} />
</Form.Item>
{form.getFieldValue('db_type') === 'omc' ? (
<Form.Item name="db_name" label="DB Name" className="mb-3" rules={[{ required: true }]}>
<Select
optionRender={(option, { index }) => {
const item = omcDBList[index] as any;
return (
<div key={option.value} className="flex flex-col">
<span className="text-[18px]">{item?.dbName}</span>
<span>
<span>env: </span>
<span className="text-gray-500">{item.env}</span>
</span>
<span>
<span>account: </span>
<span className="text-gray-500">{item.account}</span>
</span>
<span>
<span>searchName: </span>
<Tooltip title={item.searchName}>
<span className="text-gray-500">{item.searchName}</span>
</Tooltip>
</span>
</div>
);
}}
notFoundContent={omcListLoading ? <Spin size="small" /> : null}
showSearch
options={omcDBList}
onSearch={fetchOmcList}
onSelect={(searchName) => {
console.log(searchName, 'searchName');
const item = omcDBList?.find((item: any) => item.value === searchName) as any;
form.setFieldsValue({
db_arn: item?.arn,
});
}}
/>
</Form.Item>
) : (
<Form.Item name="db_name" label="DB Name" className="mb-3" rules={[{ required: true }]}>
<Input readOnly={!!editValue} disabled={!!editValue} />
</Form.Item>
)}
{fileDb === true && (
<Form.Item name="db_path" label="Path" className="mb-3" rules={[{ required: true }]}>
<Input />
</Form.Item>
)}
{fileDb === false && (
{fileDb === false && form.getFieldValue('db_type') !== 'omc' && (
<>
<Form.Item name="db_user" label="Username" className="mb-3" rules={[{ required: true }]}>
<Input />
@@ -106,7 +191,11 @@ function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClos
</Form.Item>
</>
)}
{form.getFieldValue('db_type') === 'omc' && (
<Form.Item name="db_arn" label="Arn" className="mb-3" rules={[{ required: true }]}>
<Input />
</Form.Item>
)}
<Form.Item name="comment" label="Remark" className="mb-3">
<Input />
</Form.Item>

View File

@@ -25,6 +25,8 @@ interface FlowCardProps {
}
const FlowCard: React.FC<FlowCardProps> = ({ flow, onCopy, deleteCallback }) => {
console.log(flow, 'flow');
const { model } = useContext(ChatContext);
const { t } = useTranslation();
const [modal, contextHolder] = Modal.useModal();
@@ -76,8 +78,8 @@ const FlowCard: React.FC<FlowCardProps> = ({ flow, onCopy, deleteCallback }) =>
title={flow.name}
desc={flow.description}
tags={[
{ text: flow.source, color: flow.source === 'DBGPT-WEB' ? 'green' : 'blue', border: true },
{ text: flow.editable ? 'Editable' : 'Can not Edit', color: flow.editable ? 'green' : 'gray', border: true },
{ text: flow.source, border: true, color: flow.source === 'DBGPT-WEB' ? 'green' : 'blue' },
{ text: flow.editable ? 'Editable' : 'Can not Edit', color: flow.editable ? 'green' : 'gray' },
{
text: (
<>
@@ -120,7 +122,7 @@ const FlowCard: React.FC<FlowCardProps> = ({ flow, onCopy, deleteCallback }) =>
},
]}
>
<div className="w-full h-40 shadow-[inset_0_0_16px_rgba(50,50,50,.05)]">
<div className="w-full h-[150px] shadow-[inset_0_0_16px_rgba(50,50,50,.05)]">
<FlowPreview flowData={flow.flow_data} />
</div>
</GptCard>

View File

@@ -1,12 +1,13 @@
function SplitScreenHeight() {
return (
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024" version="1.1">
<path
d="M161.05472 919.3472h701.9008a58.71616 58.71616 0 0 0 58.65472-58.65472V180.40832a58.71616 58.71616 0 0 0-58.65472-58.65472H161.09568a58.03008 58.03008 0 0 0-41.4208 17.08032A58.1632 58.1632 0 0 0 102.4 180.30592v680.38656a58.64448 58.64448 0 0 0 58.65472 58.65472z m385.15712-589.568V190.08512h306.95424v660.93056H546.21184V329.7792zM170.83392 190.08512h306.95424v660.93056H170.83392V190.08512z"
p-id="13913"
></path>
</svg>
);
return (
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024" version="1.1">
<path
d="M161.05472 919.3472h701.9008a58.71616 58.71616 0 0 0 58.65472-58.65472V180.40832a58.71616 58.71616 0 0 0-58.65472-58.65472H161.09568a58.03008 58.03008 0 0 0-41.4208 17.08032A58.1632 58.1632 0 0 0 102.4 180.30592v680.38656a58.64448 58.64448 0 0 0 58.65472 58.65472z m385.15712-589.568V190.08512h306.95424v660.93056H546.21184V329.7792zM170.83392 190.08512h306.95424v660.93056H170.83392V190.08512z"
p-id="13913"
></path>
</svg>
);
}
export default SplitScreenHeight;

View File

@@ -10,3 +10,4 @@ function SplitScreenWeight() {
}
export default SplitScreenWeight;

View File

@@ -43,7 +43,7 @@ export default function ArgumentsModal({ space, argumentsShow, setArgumentsShow
label={t('recall_score')}
name={['embedding', 'recall_score']}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_owner')} />
<Input className="mb-5 h-12" placeholder="请输入" />
</Form.Item>
</Col>
<Col span={12}>

View File

@@ -1,10 +1,12 @@
import { FileTextFilled, FileWordTwoTone, IeCircleFilled } from '@ant-design/icons';
import { FileTextFilled, FileWordTwoTone, IeCircleFilled, YuqueFilled } from '@ant-design/icons';
export default function DocIcon({ type }: { type: string }) {
if (type === 'TEXT') {
return <FileTextFilled className="text-[#2AA3FF] mr-2 !text-lg" />;
} else if (type === 'DOCUMENT') {
return <FileWordTwoTone className="text-[#2AA3FF] mr-2 !text-lg" />;
} else if (type === 'YUQUEURL') {
return <YuqueFilled className="text-[#2AA3FF] mr-2 !text-lg" />;
} else {
return <IeCircleFilled className="text-[#2AA3FF] mr-2 !text-lg" />;
}

View File

@@ -1,37 +1,101 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Button, Card, Space, Divider, Empty, Spin, Tag, Tooltip, Modal } from 'antd';
import { DeleteFilled, InteractionFilled, PlusOutlined, ToolFilled, EyeFilled, WarningOutlined, DeploymentUnitOutlined} from '@ant-design/icons';
import { apiInterceptors, delDocument, getDocumentList, syncDocument } from '@/client/api';
import {
apiInterceptors,
delDocument,
editChunk,
getDocumentList,
// getKnowledgeAdmins,
searchDocumentList,
syncDocument,
updateKnowledgeAdmins,
} from '@/client/api';
import { IDocument, ISpace } from '@/types/knowledge';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
ExperimentOutlined,
EyeOutlined,
LoadingOutlined,
MinusCircleOutlined,
PlusOutlined,
SearchOutlined,
SyncOutlined,
ToolFilled,
WarningOutlined,
DeploymentUnitOutlined,
} from '@ant-design/icons';
import { useRequest } from 'ahooks';
import { Button, Card, Divider, Dropdown, Empty, Form, Input, Modal, Select, Space, Spin, Tag, Tooltip, message, notification } from 'antd';
import cls from 'classnames';
import moment from 'moment';
import ArgumentsModal from './arguments-modal';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ArgumentsModal from './arguments-modal';
import DocIcon from './doc-icon';
import RecallTestModal from './RecallTestModal';
interface IProps {
space: ISpace;
addStatus?: string;
onAddDoc: (spaceName: string) => void;
onDeleteDoc: () => void;
}
const { confirm } = Modal;
const SyncContent: React.FC<{ name: string; id: number }> = ({ name, id }) => {
const [syncLoading, setSyncLoading] = useState<boolean>(false);
const { t } = useTranslation();
const handleSync = async (spaceName: string, id: number) => {
setSyncLoading(true);
const res = await apiInterceptors(syncDocument(spaceName, { doc_ids: [id] }));
setSyncLoading(false);
if (res[2]?.success) {
message.success(t('Synchronization_initiated'));
}
};
if (syncLoading) {
return <Spin indicator={<LoadingOutlined spin />} />;
}
return (
<Space
onClick={() => {
handleSync(name, id);
}}
>
<SyncOutlined />
<span>{t('Sync')}</span>
</Space>
);
};
export default function DocPanel(props: IProps) {
const { space } = props;
const [form] = Form.useForm();
const { space, addStatus } = props;
const { t } = useTranslation();
const router = useRouter();
const page_size = 18;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [admins, setAdmins] = useState<string[]>([]);
const [documents, setDocuments] = useState<any>([]);
const [searchDocuments, setSearchDocuments] = useState<any>([]);
const [argumentsShow, setArgumentsShow] = useState<boolean>(false);
const [total, setTotal] = useState<number>(0);
const [editOpen, setEditOpen] = useState<boolean>(false);
const [curDoc, setCurDoc] = useState<IDocument>();
// 召回测试弹窗
const [recallTestOpen, setRecallTestOpen] = useState<boolean>(false);
const currentPageRef = useRef(1);
const hasMore = useMemo(() => {
return documents.length < total;
}, [documents.length, total]);
return documents?.length < total;
}, [documents, total]);
const showDeleteConfirm = (row: any) => {
confirm({
@@ -47,24 +111,33 @@ export default function DocPanel(props: IProps) {
});
};
async function fetchDocuments() {
setIsLoading(true);
const [_, data] = await apiInterceptors(
getDocumentList(space.name, {
page: currentPageRef.current,
page_size,
}),
);
setDocuments(data?.data);
setTotal(data?.total || 0);
setIsLoading(false);
}
const {
run: fetchDocuments,
refresh,
loading: isLoading,
} = useRequest(
async () =>
await apiInterceptors(
getDocumentList(space.name, {
page: currentPageRef.current,
page_size,
}),
),
{
manual: true,
onSuccess: (res) => {
const [, data] = res;
setDocuments(data?.data);
setSearchDocuments(data?.data);
setTotal(data?.total || 0);
},
},
);
const loadMoreDocuments = async () => {
if (!hasMore) {
return;
}
setIsLoading(true);
currentPageRef.current += 1;
const [_, data] = await apiInterceptors(
getDocumentList(space.name, {
@@ -73,11 +146,7 @@ export default function DocPanel(props: IProps) {
}),
);
setDocuments([...documents, ...data!.data]);
setIsLoading(false);
};
const handleSync = async (spaceName: string, id: number) => {
await apiInterceptors(syncDocument(spaceName, { doc_ids: [id] }));
setSearchDocuments([...documents, ...data!.data]);
};
const handleDelete = async (row: any) => {
@@ -93,10 +162,10 @@ export default function DocPanel(props: IProps) {
const handleArguments = () => {
setArgumentsShow(true);
};
const openGraphVisualPage = () => {
router.push(`/knowledge/graph/?spaceName=${space.name}`);
}
const renderResultTag = (status: string, result: string) => {
let color;
switch (status) {
@@ -122,90 +191,258 @@ export default function DocPanel(props: IProps) {
</Tooltip>
);
};
// const getAdmins = useCallback(async () => {
// const [err, data] = await apiInterceptors(getKnowledgeAdmins(space.id as string));
// if (!data || !data.length) return;
// setAdmins(data as string[]);
// }, [space.id]);
useEffect(() => {
fetchDocuments();
}, [space]);
// getAdmins();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (addStatus === 'finish') {
fetchDocuments();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addStatus]);
const updateAdmins = useCallback(
async (options: string[]) => {
const { data } = await updateKnowledgeAdmins({
space_id: space.id as string,
user_nos: options as any,
});
if (!data.success) {
// getAdmins();
notification.error({ description: data.err_msg, message: 'Update Error' });
} else {
message.success(t('Edit_Success'));
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[space.id],
);
const handleChange = (value: string[]) => {
updateAdmins(value);
setAdmins(value);
};
const { run: search, loading: searchLoading } = useRequest(
async (id, doc_name: string) => {
const [, res] = await apiInterceptors(searchDocumentList(space.name, { doc_name }));
return res;
},
{
manual: true,
debounceWait: 500,
onSuccess: (data) => {
console.log(data);
setSearchDocuments(data?.data);
},
},
);
const { run: editChunkRun, loading: chunkLoading } = useRequest(
async (values: any) => {
return await editChunk(props.space.name, {
questions: values.questions?.map((item: any) => item.question),
doc_id: curDoc?.id || '',
doc_name: values.doc_name,
});
},
{
manual: true,
onSuccess: async (res) => {
if (res.data.success) {
message.success(t('Edit_Success'));
await fetchDocuments();
setEditOpen(false);
} else {
message.error(res.data.err_msg);
}
},
},
);
const renderDocumentCard = () => {
if (documents?.length > 0) {
return (
<div className="max-h-96 overflow-auto max-w-3/4">
<div className="mt-3 grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-5">
{documents.map((document: IDocument) => {
return (
<Card
key={document.id}
className=" dark:bg-[#484848] relative shrink-0 grow-0 cursor-pointer rounded-[10px] border border-gray-200 border-solid w-full"
title={
<Tooltip title={document.doc_name}>
<div className="truncate ">
<DocIcon type={document.doc_type} />
<span>{document.doc_name}</span>
</div>
</Tooltip>
}
extra={
<div className="mx-3">
<Tooltip title={'detail'}>
<EyeFilled
className="mr-2 !text-lg"
style={{ color: '#1b7eff', fontSize: '20px' }}
onClick={() => {
router.push(`/knowledge/chunk/?spaceName=${space.name}&id=${document.id}`);
}}
/>
</Tooltip>
<Tooltip title={'Sync'}>
<InteractionFilled
className="mr-2 !text-lg"
style={{ color: '#1b7eff', fontSize: '20px' }}
onClick={() => {
handleSync(space.name, document.id);
}}
/>
</Tooltip>
<Tooltip title={'Delete'}>
<DeleteFilled
className="text-[#ff1b2e] !text-lg"
onClick={() => {
showDeleteConfirm(document);
}}
/>
</Tooltip>
</div>
}
>
<p className="mt-2 font-semibold ">{t('Size')}:</p>
<p>{document.chunk_size} chunks</p>
<p className="mt-2 font-semibold ">{t('Last_Sync')}:</p>
<p>{moment(document.last_sync).format('YYYY-MM-DD HH:MM:SS')}</p>
<p className="mt-2 mb-2">{renderResultTag(document.status, document.result)}</p>
</Card>
);
})}
return (
<div className="w-full h-full">
<div className="mb-4">
{/* <div className="mb-1">管理员工号去前缀0</div> */}
<div className="flex w-full justify-end">
{/* <Select
mode="tags"
value={admins}
style={{ width: '50%' }}
onChange={handleChange}
tokenSeparators={[',']}
options={admins.map((item: string) => ({ label: item, value: item }))}
/> */}
<Button
type="primary"
onClick={async () => {
await refresh();
}}
loading={isLoading}
>
{t('Refresh_status')}
</Button>
</div>
{hasMore && (
<Divider>
<span className="cursor-pointer" onClick={loadMoreDocuments}>
{t('Load_more')}
</span>
</Divider>
</div>
<div className="flex flex-col h-full p-3 border rounded-md">
{documents?.length > 0 ? (
<>
<div className="flex flex-1 justify-between items-center">
<Input
className="w-1/3"
prefix={<SearchOutlined />}
placeholder={t('please_enter_the_keywords')}
onChange={async (e) => {
await search(space.id, e.target.value);
}}
allowClear
/>
<Button
onClick={async () => {
await apiInterceptors(syncDocument(space.name, { doc_ids: [] }));
message.success(t('Synchronization_initiated'));
}}
className="border"
>
{t('synchronization')}
</Button>
</div>
<Spin spinning={searchLoading}>
<>
{searchDocuments.length > 0 ? (
<div className="h-96 mt-3 grid grid-cols-3 gap-x-6 gap-y-5 overflow-y-auto">
{searchDocuments.map((document: IDocument) => {
return (
<Card
key={document.id}
className=" dark:bg-[#484848] relative shrink-0 grow-0 cursor-pointer rounded-[10px] border border-gray-200 border-solid w-full max-h-64"
title={
<Tooltip title={document.doc_name}>
<div className="truncate ">
<DocIcon type={document.doc_type} />
<span>{document.doc_name}</span>
</div>
</Tooltip>
}
extra={
<Dropdown
menu={{
items: [
{
key: 'publish',
label: (
<Space
onClick={() => {
router.push(`/construct/knowledge/chunk/?spaceName=${space.name}&id=${document.id}`);
}}
>
<EyeOutlined />
<span>{t('detail')}</span>
</Space>
),
},
{
key: `${t('Sync')}`,
label: <SyncContent name={space.name} id={document.id} />,
},
{
key: 'edit',
label: (
<Space
onClick={() => {
setEditOpen(true);
setCurDoc(document);
}}
>
<EditOutlined />
<span>{t('Edit')}</span>
</Space>
),
},
{
key: 'del',
label: (
<Space
onClick={() => {
showDeleteConfirm(document);
}}
>
<DeleteOutlined />
<span>{t('Delete')}</span>
</Space>
),
},
],
}}
getPopupContainer={(node) => node.parentNode as HTMLElement}
placement="bottomRight"
autoAdjustOverflow={false}
className="bg-gray-100 rounded-md"
>
<EllipsisOutlined className="p-2" />
</Dropdown>
}
>
<p className="mt-2 font-semibold ">{t('Size')}:</p>
<p>{document.chunk_size} chunks</p>
<p className="mt-2 font-semibold ">{t('Last_Sync')}:</p>
<p>{moment(document.last_sync).format('YYYY-MM-DD HH:MM:SS')}</p>
<p className="mt-2 mb-2">{renderResultTag(document.status, document.result)}</p>
</Card>
);
})}
</div>
) : (
<Empty className="flex flex-1 w-full py-10 flex-col items-center justify-center" image={Empty.PRESENTED_IMAGE_DEFAULT} />
)}
</>
{hasMore && (
<Divider>
<span className="cursor-pointer" onClick={loadMoreDocuments}>
{t('Load_more')}
</span>
</Divider>
)}
</Spin>
</>
) : (
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT}>
<Button type="primary" className="flex items-center mx-auto" icon={<PlusOutlined />} onClick={handleAddDocument}>
Create Now
</Button>
</Empty>
)}
</div>
);
}
return (
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT}>
<Button type="primary" className="flex items-center mx-auto" icon={<PlusOutlined />} onClick={handleAddDocument}>
Create Now
</Button>
</Empty>
</div>
);
};
useEffect(() => {
if (!curDoc) {
return;
}
form.setFieldsValue({
doc_name: curDoc.doc_name,
questions: curDoc.questions?.map((ques) => {
return {
question: ques,
};
}),
});
}, [curDoc, form]);
return (
<div className="collapse-container pt-2 px-4">
<div className="px-4">
<Space>
<Button size="middle" type="primary" className="flex items-center" icon={<PlusOutlined />} onClick={handleAddDocument}>
{t('Add_Datasource')}
@@ -216,10 +453,88 @@ export default function DocPanel(props: IProps) {
{
space.vector_type === 'KnowledgeGraph' && (<Button size="middle" className="flex items-center mx-2" icon={<DeploymentUnitOutlined />} onClick={openGraphVisualPage}>{t('View_Graph')}</Button>)
}
<Button icon={<ExperimentOutlined />} onClick={() => setRecallTestOpen(true)}>
{t('Recall_test')}
</Button>
</Space>
<Divider />
<Spin spinning={isLoading}>{renderDocumentCard()}</Spin>
<ArgumentsModal space={space} argumentsShow={argumentsShow} setArgumentsShow={setArgumentsShow} />
{/* 编辑弹窗 */}
<Modal
title={t('Edit_document')}
open={editOpen}
onCancel={() => setEditOpen(false)}
destroyOnClose={true}
footer={[
<Button key="back" onClick={() => setEditOpen(false)}>
{t('cancel')}
</Button>,
<Button
key="submit"
type="primary"
loading={chunkLoading}
onClick={async () => {
const values = form.getFieldsValue();
await editChunkRun(values);
}}
>
{t('verify')}
</Button>,
]}
>
<Form
form={form}
initialValues={{
doc_name: curDoc?.doc_name,
questions: curDoc?.questions?.map((ques) => {
return {
question: ques,
};
}),
}}
>
<Form.Item label={t('Document_name')} name="doc_name">
<Input />
</Form.Item>
<Form.Item label={t('Correlation_problem')}>
<Form.List name="questions">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name }, index) => (
<div key={key} className={cls('flex flex-1 items-center gap-8 mb-6')}>
<Form.Item label="" name={[name, 'question']} className="grow">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add({ question: '', valid: false });
}}
block
icon={<PlusOutlined />}
>
{t('Add_problem')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</Form>
</Modal>
{/* 召回测试弹窗 */}
<RecallTestModal open={recallTestOpen} setOpen={setRecallTestOpen} space={space} />
</div>
);
}

View File

@@ -29,6 +29,12 @@ export default function DocTypeForm(props: IProps) {
subTitle: t('Upload_a_document'),
iconType: 'DOCUMENT',
},
{
type: 'YUQUEURL',
title: t('yuque'),
subTitle: t('Get_yuque_document'),
iconType: 'YUQUEURL',
},
];
return (

View File

@@ -1,12 +1,12 @@
import { Button, Form, Input, Upload, Spin, message } from 'antd';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InboxOutlined } from '@ant-design/icons';
import { apiInterceptors, addDocument, uploadDocument } from '@/client/api';
import { addDocument, apiInterceptors, uploadDocument, addYuque } from '@/client/api';
import { StepChangeParams } from '@/types/knowledge';
import { InboxOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Spin, Upload, message } from 'antd';
import { RcFile, UploadChangeParam } from 'antd/es/upload';
import { File, StepChangeParams } from '@/types/knowledge';
import { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface';
import classNames from 'classnames';
import { default as classNames, default as cls } from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Typography } from 'antd';
type FileParams = {
file: RcFile;
@@ -26,6 +26,8 @@ type FieldType = {
originFileObj: FileParams;
text: string;
webPageUrl: string;
questions: Record<string, any>[];
doc_token?: string;
};
const { Dragger } = Upload;
@@ -36,11 +38,11 @@ export default function DocUploadForm(props: IProps) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [spinning, setSpinning] = useState<boolean>(false);
const [files, setFiles] = useState<Array<File>>([]);
const [files, setFiles] = useState<any>([]);
const upload = async (data: FieldType) => {
const { docName, textSource, text, webPageUrl } = data;
let docId;
const { docName, textSource, text, webPageUrl, doc_token, questions = [], originFileObj } = data;
let docId: any;
setSpinning(true);
switch (docType) {
case 'URL':
@@ -49,6 +51,7 @@ export default function DocUploadForm(props: IProps) {
doc_name: docName,
content: webPageUrl,
doc_type: 'URL',
questions: questions?.map((item) => item.question),
}),
);
break;
@@ -59,9 +62,43 @@ export default function DocUploadForm(props: IProps) {
source: textSource,
content: text,
doc_type: 'TEXT',
questions: questions.map((item) => item.question),
}),
);
break;
case 'YUQUEURL':
[, docId] = await apiInterceptors(
addYuque({
doc_name: docName,
space_name: spaceName,
content: webPageUrl,
doc_type: 'YUQUEURL',
doc_token: doc_token || '',
questions: questions?.map((item) => item.question),
}),
);
break;
case 'DOCUMENT':
const file = originFileObj as any;
const formData = new FormData();
const filename = file?.name;
const ques = questions.map((item) => item.question);
formData.append('doc_name', filename);
formData.append('doc_file', file);
formData.append('doc_type', 'DOCUMENT');
formData.append('questions', JSON.stringify(ques));
[, docId] = await apiInterceptors(uploadDocument(spaceName, formData));
console.log(docId);
if (Number.isInteger(docId)) {
setFiles((files: any) => {
files.push({
name: filename,
doc_id: docId || -1,
});
return files;
});
}
break;
}
setSpinning(false);
if (docType === 'DOCUMENT' && files.length < 1) {
@@ -86,28 +123,8 @@ export default function DocUploadForm(props: IProps) {
const handleFileChange = ({ file, fileList }: UploadChangeParam) => {
if (fileList.length === 0) {
form.setFieldValue('originFileObj', null);
}
};
const uploadFile = async (options: RcCustomRequestOptions) => {
const { onSuccess, onError, file } = options;
const formData = new FormData();
const filename = file?.name;
formData.append('doc_name', filename);
formData.append('doc_file', file);
formData.append('doc_type', 'DOCUMENT');
const [, docId] = await apiInterceptors(uploadDocument(spaceName, formData));
if (Number.isInteger(docId)) {
onSuccess && onSuccess(docId || 0);
setFiles((files) => {
files.push({
name: filename,
doc_id: docId || -1,
});
return files;
});
} else {
onError && onError({ name: '', message: '' });
form.setFieldValue('originFileObj', file);
}
};
@@ -127,6 +144,40 @@ export default function DocUploadForm(props: IProps) {
<Form.Item<FieldType> label={`${t('Text')}:`} name="text" rules={[{ required: true, message: t('Please_input_the_description') }]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item<FieldType> label={`${t('Correlation_problem')}:`}>
<Form.List name="questions">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name }, index) => (
<div key={key} className={cls('flex flex-1 items-center gap-8 mb-6')}>
<Form.Item label="" name={[name, 'question']} className="grow">
<Input placeholder={t('input_question')} />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
block
icon={<PlusOutlined />}
>
{t('Add_problem')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</>
);
};
@@ -144,6 +195,101 @@ export default function DocUploadForm(props: IProps) {
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_Web_Page_URL')} />
</Form.Item>
<Form.Item<FieldType> label={`${t('Correlation_problem')}:`}>
<Form.List name="questions">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name }, index) => (
<div key={key} className={cls('flex flex-1 items-center gap-8 mb-6')}>
<Form.Item label="" name={[name, 'question']} className="grow">
<Input placeholder={t('input_question')} />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
block
icon={<PlusOutlined />}
>
{t('Add_problem')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</>
);
};
const renderYuquePage = () => {
return (
<>
<Form.Item<FieldType> label={`${t('Name')}:`} name="docName" rules={[{ required: true, message: t('Please_input_the_name') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_name')} />
</Form.Item>
<Form.Item<FieldType> label={t('document_url')} name="webPageUrl" rules={[{ required: true, message: t('input_document_url') }]}>
<Input className="mb-5 h-12" placeholder={t('input_document_url')} />
</Form.Item>
<Form.Item<FieldType>
label={t('document_token')}
name="doc_token"
tooltip={
<>
{t('Get_token')}
<Typography.Link href="https://yuque.antfin-inc.com/lark/openapi/dh8zp4" target="_blank">
{t('Reference_link')}
</Typography.Link>
</>
}
>
<Input className="mb-5 h-12" placeholder={t('input_document_token')} />
</Form.Item>
<Form.Item<FieldType> label={`${t('Correlation_problem')}:`}>
<Form.List name="questions">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name }, index) => (
<div key={key} className={cls('flex flex-1 items-center gap-8 mb-6')}>
<Form.Item label="" name={[name, 'question']} className="grow">
<Input placeholder={t('input_question')} />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
block
icon={<PlusOutlined />}
>
{t('Add_problem')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</>
);
};
@@ -154,20 +300,54 @@ export default function DocUploadForm(props: IProps) {
<Form.Item<FieldType> name="originFileObj" rules={[{ required: true, message: t('Please_select_file') }]}>
<Dragger
multiple
beforeUpload={() => false}
onChange={handleFileChange}
maxCount={10}
accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.txt,.md"
customRequest={uploadFile}
maxCount={1}
accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.txt,.md,.zip"
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p style={{ color: 'rgb(22, 108, 255)', fontSize: '20px' }}>{t('Select_or_Drop_file')}</p>
<p className="ant-upload-hint" style={{ color: 'rgb(22, 108, 255)' }}>
PDF, PowerPoint, Excel, Word, Text, Markdown,
PDF, PowerPoint, Excel, Word, Text, Markdown, Zip1
</p>
</Dragger>
</Form.Item>
<Form.Item<FieldType> label="关联问题:">
<Form.List name="questions">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name }, index) => (
<div key={key} className={cls('flex flex-1 items-center gap-8 mb-6')}>
<Form.Item label="" name={[name, 'question']} className="grow">
<Input placeholder="请输入问题" />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
block
icon={<PlusOutlined />}
>
{t('Add_problem')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</>
);
};
@@ -178,6 +358,8 @@ export default function DocUploadForm(props: IProps) {
return renderWebPage();
case 'DOCUMENT':
return renderDocument();
case 'YUQUEURL':
return renderYuquePage();
default:
return renderText();
}

View File

@@ -22,6 +22,7 @@ let intervalId: string | number | NodeJS.Timeout | undefined;
export default function Segmentation(props: IProps) {
const { spaceName, docType, uploadFiles, handleStepChange } = props;
console.log(docType, 'doctype');
const { t } = useTranslation();
const [form] = Form.useForm();
const [files, setFiles] = useState(uploadFiles);
@@ -29,6 +30,7 @@ export default function Segmentation(props: IProps) {
const [strategies, setStrategies] = useState<Array<IChunkStrategyResponse>>([]);
const [syncStatus, setSyncStatus] = useState<string>('');
const spaceId = localStorage.getItem('cur_space_id');
async function getStrategies() {
setLoading(true);
const [, allStrategies] = await apiInterceptors(getChunkStrategies());
@@ -41,6 +43,7 @@ export default function Segmentation(props: IProps) {
return () => {
intervalId && clearInterval(intervalId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleFinish = async (data: FieldType) => {
@@ -61,6 +64,11 @@ export default function Segmentation(props: IProps) {
handleStepChange({
label: 'finish',
});
} else if (status === 'FAILED') {
clearInterval(intervalId);
handleStepChange({
label: 'finish',
});
}
}, 3000);
}
@@ -77,8 +85,8 @@ export default function Segmentation(props: IProps) {
fileStrategies.map((item) => {
const name = item?.chunk_parameters?.chunk_strategy;
if (!name) {
// set default strategy
item.chunk_parameters = { chunk_strategy: 'Automatic' };
message.error(`Please select chunk strategy for ${item.name}.`);
checked = false;
}
const strategy = strategies.filter((item) => item.strategy === name)[0];
const newParam: any = {
@@ -98,7 +106,7 @@ export default function Segmentation(props: IProps) {
async function updateSyncStatus(docIds: Array<number>) {
const [, docs] = await apiInterceptors(
getDocumentList(spaceName, {
getDocumentList(spaceName as any, {
doc_ids: docIds,
}),
);
@@ -129,8 +137,9 @@ export default function Segmentation(props: IProps) {
switch (docType) {
case 'TEXT':
case 'URL':
case 'YUQUEURL':
return fields?.map((field) => (
<StrategyForm strategies={strategies} docType={docType} fileName={files![field.name].name} field={field} />
<StrategyForm key={field.key} strategies={strategies} docType={docType} fileName={files![field.name].name} field={field} />
));
case 'DOCUMENT':
return (

View File

@@ -8,6 +8,7 @@ import moment from 'moment';
import { apiInterceptors, delSpace, newDialogue } from '@/client/api';
import { useTranslation } from 'react-i18next';
import GptCard from '../common/gpt-card';
import IconFont from '@/ant-components/common/Icon';
interface IProps {
space: ISpace;

View File

@@ -1,7 +1,7 @@
import { addSpace, apiInterceptors } from '@/client/api';
import { IStorage, StepChangeParams } from '@/types/knowledge';
import { StepChangeParams, IStorage } from '@/types/knowledge';
import { Button, Form, Input, Spin, Select } from 'antd';
import { useEffect, useState } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
type FieldType = {
@@ -50,6 +50,7 @@ export default function SpaceForm(props: IProps) {
);
setSpinning(false);
const is_financial = domain_type === 'FinancialReport';
localStorage.setItem('cur_space_id', JSON.stringify(data));
res?.success && handleStepChange({ label: 'forward', spaceName, pace: is_financial ? 2 : 1, docType: is_financial ? 'DOCUMENT' : '' });
};
@@ -80,14 +81,11 @@ export default function SpaceForm(props: IProps) {
}),
]}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_name')} />
</Form.Item>
<Form.Item<FieldType> label={t('Owner')} name="owner" rules={[{ required: true, message: t('Please_input_the_owner') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_owner')} />
<Input className="h-12" placeholder={t('Please_input_the_name')} />
</Form.Item>
<Form.Item<FieldType> label={t('Storage')} name="storage" rules={[{ required: true, message: t('Please_select_the_storage') }]}>
<Select className="mb-5 h-12" placeholder={t('Please_select_the_storage')} onChange={handleStorageChange}>
{spaceConfig?.map((item) => {
{spaceConfig?.map((item: any) => {
return <Select.Option value={item.name}>{item.desc}</Select.Option>;
})}
</Select>
@@ -95,14 +93,14 @@ export default function SpaceForm(props: IProps) {
<Form.Item<FieldType> label={t('Domain')} name="field" rules={[{ required: true, message: t('Please_select_the_domain_type') }]}>
<Select className="mb-5 h-12" placeholder={t('Please_select_the_domain_type')}>
{spaceConfig
?.find((item) => item.name === storage)
?.domain_types.map((item) => {
?.find((item: any) => item.name === storage)
?.domain_types.map((item: any) => {
return <Select.Option value={item.name}>{item.desc}</Select.Option>;
})}
</Select>
</Form.Item>
<Form.Item<FieldType> label={t('Description')} name="description" rules={[{ required: true, message: t('Please_input_the_description') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_description')} />
<Input className="h-12" placeholder={t('Please_input_the_description')} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">

View File

@@ -38,10 +38,10 @@ export default function StrategyForm({ strategies, docType, fileName, field }: I
if (!selectedStrategy) {
return null;
}
if (selectedStrategy === DEFAULT_STRATEGY.strategy) {
if (selectedStrategy === DEFAULT_STRATEGY.name) {
return <p className="my-4">{DEFAULT_STRATEGY.desc}</p>;
}
const parameters = ableStrategies?.filter((i) => i.strategy === selectedStrategy)[0].parameters;
const parameters = ableStrategies?.filter((i) => i.strategy === selectedStrategy)[0]?.parameters;
if (!parameters || !parameters.length) {
return <Alert className="my-2" type="warning" message={t('No_parameter')} />;
}

View File

@@ -1,31 +1,30 @@
import UserBar from '@/ant-components/layout/UserBar';
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, delDialogue } from '@/client/api';
import { STORAGE_LANG_KEY, STORAGE_THEME_KEY } from '@/utils';
import { DarkSvg, SunnySvg, ModelSvg } from '@/components/icons';
import { DarkSvg, ModelSvg, SunnySvg } from '@/components/icons';
import { IChatDialogueSchema } from '@/types/chat';
import { STORAGE_LANG_KEY, STORAGE_THEME_KEY, STORAGE_USERINFO_KEY } from '@/utils/constants/index';
import Icon, {
AppstoreOutlined,
BuildOutlined,
ConsoleSqlOutlined,
PartitionOutlined,
DeleteOutlined,
MessageOutlined,
ForkOutlined,
GlobalOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
PlusOutlined,
ShareAltOutlined,
MenuOutlined,
SettingOutlined,
BuildOutlined,
ForkOutlined,
AppstoreOutlined,
MessageOutlined,
PartitionOutlined
} from '@ant-design/icons';
import { Modal, message, Tooltip, Dropdown } from 'antd';
import { Modal, Popover, Tooltip, message } from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems';
import cls from 'classnames';
import copy from 'copy-to-clipboard';
import moment from 'moment';
import 'moment/locale/zh-cn';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { ReactNode, useCallback, useContext, useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
type SettingItem = {
@@ -33,7 +32,11 @@ type SettingItem = {
name: string;
icon: ReactNode;
noDropdownItem?: boolean;
onClick: () => void;
onClick?: () => void;
items?: ItemType[];
onSelect?: (p: { key: string }) => void;
defaultSelectedKeys?: string[];
placement?: 'top' | 'topLeft';
};
type RouteItem = {
@@ -41,10 +44,11 @@ type RouteItem = {
name: string;
icon: ReactNode;
path: string;
isActive?: boolean;
};
function menuItemStyle(active?: boolean) {
return `flex items-center h-10 hover:bg-[#F1F5F9] dark:hover:bg-theme-dark text-base w-full transition-colors whitespace-nowrap px-4 ${
return `flex items-center h-12 hover:bg-[#F1F5F9] dark:hover:bg-theme-dark text-base w-full transition-colors whitespace-nowrap px-4 ${
active ? 'bg-[#F1F5F9] dark:bg-theme-dark' : ''
}`;
}
@@ -56,13 +60,17 @@ function smallMenuItemStyle(active?: boolean) {
}
function SideBar() {
const { chatId, scene, isMenuExpand, dialogueList, queryDialogueList, refreshDialogList, setIsMenuExpand, setAgent, mode, setMode } =
const { chatId, scene, isMenuExpand, dialogueList, queryDialogueList, refreshDialogList, setIsMenuExpand, setAgent, mode, setMode, adminList } =
useContext(ChatContext);
const { pathname, replace } = useRouter();
const { t, i18n } = useTranslation();
const [logo, setLogo] = useState<string>('/logo_zh_latest.png');
const [logo, setLogo] = useState<string>('/LOGO_1.png');
const hasAdmin = useMemo(() => {
const { user_id } = JSON.parse(localStorage.getItem(STORAGE_USERINFO_KEY) || '{}');
return adminList.some((admin) => admin.user_id === user_id);
}, [adminList]);
const routes = useMemo(() => {
const items: RouteItem[] = [
{
@@ -109,37 +117,133 @@ function SideBar() {
},
];
return items;
}, [i18n.language]);
}, [t]);
const handleToggleMenu = () => {
const handleToggleMenu = useCallback(() => {
setIsMenuExpand(!isMenuExpand);
};
}, [isMenuExpand, setIsMenuExpand]);
const handleToggleTheme = useCallback(() => {
const theme = mode === 'light' ? 'dark' : 'light';
setMode(theme);
localStorage.setItem(STORAGE_THEME_KEY, theme);
}, [mode]);
}, [mode, setMode]);
const handleChangeLang = useCallback(() => {
const language = i18n.language === 'en' ? 'zh' : 'en';
i18n.changeLanguage(language);
if (language === 'zh') moment.locale('zh-cn');
if (language === 'en') moment.locale('en');
localStorage.setItem(STORAGE_LANG_KEY, language);
}, [i18n.language, i18n.changeLanguage]);
}, [i18n]);
const settings = useMemo(() => {
const items: SettingItem[] = [
{
key: 'theme',
name: t('Theme'),
icon: mode === 'dark' ? <Icon component={DarkSvg} /> : <Icon component={SunnySvg} />,
items: [
{
key: 'light',
label: (
<div className="py-1 flex justify-between gap-8 ">
<span className="flex gap-2 items-center">
<Image src="/pictures/theme_light.png" alt="english" width={38} height={32}></Image>
<span>Light</span>
</span>
<span
className={cls({
block: mode === 'light',
hidden: mode !== 'light',
})}
>
</span>
</div>
),
},
{
key: 'dark',
label: (
<div className="py-1 flex justify-between gap-8 ">
<span className="flex gap-2 items-center">
<Image src="/pictures/theme_dark.png" alt="english" width={38} height={32}></Image>
<span>Dark</span>
</span>
<span
className={cls({
block: mode === 'dark',
hidden: mode !== 'dark',
})}
>
</span>
</div>
),
},
],
onClick: handleToggleTheme,
onSelect: ({ key }: { key: string }) => {
if (mode === key) return;
setMode(key as 'light' | 'dark');
localStorage.setItem(STORAGE_THEME_KEY, key);
},
defaultSelectedKeys: [mode],
placement: 'topLeft',
},
{
key: 'language',
name: t('language'),
icon: <GlobalOutlined />,
items: [
{
key: 'en',
label: (
<div className="py-1 flex justify-between gap-8 ">
<span className="flex gap-2">
<Image src="/icons/english.png" alt="english" width={21} height={21}></Image>
<span>English</span>
</span>
<span
className={cls({
block: i18n.language === 'en',
hidden: i18n.language !== 'en',
})}
>
</span>
</div>
),
},
{
key: 'zh',
label: (
<div className="py-1 flex justify-between gap-8 ">
<span className="flex gap-2">
<Image src="/icons/zh.png" alt="english" width={21} height={21}></Image>
<span></span>
</span>
<span
className={cls({
block: i18n.language === 'zh',
hidden: i18n.language !== 'zh',
})}
>
</span>
</div>
),
},
],
onSelect: ({ key }: { key: string }) => {
if (i18n.language === key) return;
i18n.changeLanguage(key);
if (key === 'zh') moment.locale('zh-cn');
if (key === 'en') moment.locale('en');
localStorage.setItem(STORAGE_LANG_KEY, key);
},
onClick: handleChangeLang,
defaultSelectedKeys: [i18n.language],
},
{
key: 'fold',
@@ -150,7 +254,75 @@ function SideBar() {
},
];
return items;
}, [mode, handleChangeLang, handleToggleMenu, handleChangeLang]);
}, [t, mode, handleToggleTheme, i18n, handleChangeLang, isMenuExpand, handleToggleMenu, setMode]);
const functions = useMemo(() => {
const items: RouteItem[] = [
{
key: 'chat',
name: t('chat_online'),
icon: (
<Image
key="image_chat"
src={pathname === '/chat' ? '/pictures/chat_active.png' : '/pictures/chat.png'}
alt="chat_image"
width={40}
height={40}
/>
),
path: '/chat',
isActive: pathname.startsWith('/chat'),
},
{
key: 'explore',
name: t('explore'),
isActive: pathname === '/',
icon: (
<Image
key="image_explore"
src={pathname === '/' ? '/pictures/explore_active.png' : '/pictures/explore.png'}
alt="construct_image"
width={40}
height={40}
/>
),
path: '/',
},
{
key: 'construct',
name: t('construct'),
isActive: pathname.startsWith('/construct'),
icon: (
<Image
key="image_construct"
src={pathname.startsWith('/construct') ? '/pictures/app_active.png' : '/pictures/app.png'}
alt="construct_image"
width={40}
height={40}
/>
),
path: '/construct/app',
},
];
if (hasAdmin) {
items.push({
key: 'evaluation',
name: '场景评测',
icon: (
<Image
key="image_construct"
src={pathname.startsWith('/evaluation') ? '/pictures/app_active.png' : '/pictures/app.png'}
alt="construct_image"
width={40}
height={40}
/>
),
path: '/evaluation',
isActive: pathname === '/evaluation',
});
}
return items;
}, [t, pathname, hasAdmin]);
const dropDownRoutes: ItemType[] = useMemo(() => {
return routes.map<ItemType>((item) => ({
@@ -178,6 +350,18 @@ function SideBar() {
}));
}, [settings]);
const dropDownFunctions: ItemType[] = useMemo(() => {
return functions.map<ItemType>((item) => ({
key: item.key,
label: (
<Link href={item.path} className="text-base">
{item.icon}
<span className="ml-2 text-sm">{item.name}</span>
</Link>
),
}));
}, [functions]);
const handleDelChat = useCallback(
(dialogue: IChatDialogueSchema) => {
Modal.confirm({
@@ -204,7 +388,7 @@ function SideBar() {
},
});
},
[refreshDialogList],
[chatId, refreshDialogList, replace, scene],
);
const handleClickChatItem = (item: IChatDialogueSchema) => {
@@ -218,56 +402,42 @@ function SideBar() {
message[success ? 'success' : 'error'](success ? 'Copy success' : 'Copy failed');
}, []);
useEffect(() => {
queryDialogueList();
}, []);
// useEffect(() => {
// queryDialogueList();
// }, [queryDialogueList]);
useEffect(() => {
setLogo(mode === 'dark' ? '/WHITE_LOGO.png' : '/LOGO_1.png');
}, [mode]);
const language = i18n.language;
if (language === 'zh') moment.locale('zh-cn');
if (language === 'en') moment.locale('en');
}, [])
// useEffect(() => {
// setLogo(mode === 'dark' ? '/WHITE_LOGO.png' : '/logo_zh_latest.png');
// }, [mode]);
if (!isMenuExpand) {
return (
<div className="flex flex-col justify-between h-screen bg-white dark:bg-[#232734] animate-fade animate-duration-300">
<Link href="/" className="px-2 py-3">
<Image src="/LOGO_SMALL.png" alt="DB-GPT" width={63} height={46} className="w-[63px] h-[46px]" />
</Link>
<div
className="flex flex-col justify-between pt-4 h-screen bg-bar dark:bg-[#232734] animate-fade animate-duration-300"
// onMouseEnter={() => {
// setIsMenuExpand(true);
// }}
>
<div>
<Link href="/" className="flex items-center justify-center my-4 mx-auto w-12 h-12 bg-theme-primary rounded-full text-white">
<PlusOutlined className="text-lg" />
<Link href="/" className="flex justify-center items-center pb-4">
<Image src="/logo_s_latest.png" alt="DB-GPT" width={40} height={40} />
</Link>
</div>
{/* Chat List */}
<div className="flex-1 overflow-y-scroll py-4 space-y-2">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
return (
<Tooltip key={item.conv_uid} title={item.user_name || item.user_input} placement="right">
<Link
href={`/chat?scene=${item.chat_mode}&id=${item.conv_uid}`}
className={smallMenuItemStyle(active)}
onClick={() => {
handleClickChatItem(item);
}}
>
<MessageOutlined />
</Link>
</Tooltip>
);
})}
<div className="flex flex-col gap-4 items-center">
{functions.map((i) => (
<Link key={i.key} className="h-12 flex items-center" href={i.path}>
{i?.icon}
</Link>
))}
</div>
</div>
<div className="py-4">
<Dropdown menu={{ items: dropDownRoutes }} placement="topRight">
<div className={smallMenuItemStyle()}>
<MenuOutlined />
</div>
</Dropdown>
<Dropdown menu={{ items: dropDownSettings }} placement="topRight">
<div className={smallMenuItemStyle()}>
<SettingOutlined />
</div>
</Dropdown>
<UserBar onlyAvatar />
{settings
.filter((item) => item.noDropdownItem)
.map((item) => (
@@ -283,72 +453,67 @@ function SideBar() {
}
return (
<div className="flex flex-col h-screen bg-white dark:bg-[#232734]">
{/* LOGO */}
<Link href="/" className="p-2">
<Image src={logo} alt="DB-GPT" width={239} height={60} className="w-full h-full" />
</Link>
<Link href="/" className="flex items-center justify-center mb-4 mx-4 h-11 bg-theme-primary rounded text-white">
<PlusOutlined className="mr-2" />
<span>{t('new_chat')}</span>
</Link>
{/* Chat List */}
<div className="flex-1 overflow-y-scroll">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
return (
<Link
key={item.conv_uid}
href={`/chat?scene=${item.chat_mode}&id=${item.conv_uid}`}
className={`group/item ${menuItemStyle(active)}`}
onClick={() => {
handleClickChatItem(item);
}}
>
<MessageOutlined className="text-base" />
<div className="flex-1 line-clamp-1 mx-2 text-sm">{item.user_name || item.user_input}</div>
<div
className="group-hover/item:opacity-100 cursor-pointer opacity-0 mr-1"
onClick={(e) => {
e.preventDefault();
copyLink(item);
}}
<div
className="flex flex-col justify-between h-screen px-4 pt-4 bg-bar dark:bg-[#232734] animate-fade animate-duration-300"
// onMouseLeave={() => {
// setIsMenuExpand(false);
// }}
>
<div>
{/* LOGO */}
<Link href="/" className="flex items-center justify-center p-2 pb-4">
<Image src={logo} alt="DB-GPT" width={180} height={40} />
</Link>
{/* functions */}
<div className="flex flex-col gap-4">
{functions.map((item) => {
return (
<Link
href={item.path}
className={cls('flex items-center w-full h-12 px-4 cursor-pointer hover:bg-[#F1F5F9] dark:hover:bg-theme-dark hover:rounded-xl', {
'bg-white rounded-xl dark:bg-black': item.isActive,
})}
key={item.key}
>
<ShareAltOutlined />
</div>
<div
className="group-hover/item:opacity-100 cursor-pointer opacity-0"
onClick={(e) => {
e.preventDefault();
handleDelChat(item);
}}
>
<DeleteOutlined />
</div>
</Link>
);
})}
<div className="mr-3">{item.icon}</div>
<span className="text-sm">{t(item.name as any)}</span>
</Link>
);
})}
</div>
</div>
{/* Settings */}
<div className="pt-4">
<div className="max-h-52 overflow-y-auto scrollbar-default">
{routes.map((item) => (
<Link key={item.key} href={item.path} className={`${menuItemStyle(pathname === item.path)} overflow-hidden`}>
<>
{item.icon}
<span className="ml-3 text-sm">{item.name}</span>
</>
</Link>
))}
</div>
<div className="flex items-center justify-around py-4 mt-2">
<span className={cls('flex items-center w-full h-12 px-4 bg-[#F1F5F9] dark:bg-theme-dark rounded-xl')}>
<div className="mr-3 w-full">
<UserBar />
</div>
</span>
<div className="flex items-center justify-around py-4 mt-2 border-t border-dashed border-gray-200 dark:border-gray-700">
{settings.map((item) => (
<Tooltip key={item.key} title={item.name}>
<div className="flex-1 flex items-center justify-center cursor-pointer text-xl" onClick={item.onClick}>
{item.icon}
</div>
</Tooltip>
<div key={item.key}>
<Popover content={item.name}>
<div className="flex-1 flex items-center justify-center cursor-pointer text-xl" onClick={item.onClick}>
{item.icon}
</div>
</Popover>
{/* {item.items ? (
<Dropdown
menu={{ items: item.items, selectable: true, onSelect: item.onSelect, defaultSelectedKeys: item.defaultSelectedKeys }}
placement={item.placement || 'top'}
arrow
>
<span onClick={item.onClick}>{item.icon}</span>
</Dropdown>
) : (
<Popover content={item.name}>
<div className="flex-1 flex items-center justify-center cursor-pointer text-xl" onClick={item.onClick}>
{item.icon}
</div>
</Popover>
)} */}
</div>
))}
</div>
</div>

View File

@@ -39,9 +39,9 @@ if (typeof window !== 'undefined' && typeof window?.fetch === 'function') {
if (activeRequests === 0) {
load();
}
activeRequests++;
try {
const response = await originalFetch(...args);
return response;

View File

@@ -48,19 +48,19 @@ function ModelCard({ info }: Props) {
info.model_type,
]}
icon={MODEL_ICON_MAP[info.model_name]?.icon || '/models/huggingface.svg'}
operations={[
{
children: (
<div>
<PauseCircleOutlined className="mr-2" />
<span className="text-sm">Stop Model</span>
</div>
),
onClick: () => {
stopTheModel(info);
},
},
]}
// operations={[
// {
// children: (
// <div>
// <PauseCircleOutlined className="mr-2" />
// <span className="text-sm">Stop Model</span>
// </div>
// ),
// onClick: () => {
// stopTheModel(info);
// },
// },
// ]}
>
<div className="flex flex-col gap-1 px-4 pb-4 text-xs">
<div className="flex overflow-hidden">

View File

@@ -1,68 +0,0 @@
import { extendTheme } from '@mui/joy/styles';
import colors from '@mui/joy/colors';
export const joyTheme = extendTheme({
colorSchemes: {
light: {
palette: {
mode: 'dark',
primary: {
...colors.grey,
solidBg: '#e6f4ff',
solidColor: '#1677ff',
solidHoverBg: '#e6f4ff',
},
neutral: {
plainColor: '#4d4d4d',
plainHoverColor: '#131318',
plainHoverBg: '#EBEBEF',
plainActiveBg: '#D8D8DF',
plainDisabledColor: '#B9B9C6',
},
background: {
body: '#F7F7F7',
surface: '#fff',
},
text: {
primary: '#505050',
},
},
},
dark: {
palette: {
mode: 'light',
primary: {
...colors.grey,
softBg: '#353539',
softHoverBg: '#35353978',
softDisabledBg: '#353539',
solidBg: '#51525beb',
solidHoverBg: '#51525beb',
},
neutral: {
plainColor: '#D8D8DF',
plainHoverColor: '#F7F7F8',
plainHoverBg: '#353539',
plainActiveBg: '#434356',
plainDisabledColor: '#434356',
outlinedBorder: '#353539',
outlinedHoverBorder: '#454651',
},
text: {
primary: '#FDFDFC',
},
background: {
body: '#151622',
surface: '#51525beb',
},
},
},
},
fontFamily: {
body: 'Josefin Sans, sans-serif',
display: 'Josefin Sans, sans-serif',
},
zIndex: {
modal: 1001,
},
});

2
web/global.d.ts vendored
View File

@@ -13,4 +13,4 @@ declare namespace JSX {
}
}
declare module 'cytoscape-euler';
declare module 'cytoscape-euler';

Some files were not shown because too many files have changed in this diff Show More