You've already forked DataMate
Integrated Redux for state management with auth and settings slices. (#117)
* feat: Implement DatasetFileTransfer component for file selection and management * feat: Add pagination support to file list in Overview component * feat: add DatasetFileTransfer and TagManagement components - Added DatasetFileTransfer component for managing dataset files. - Introduced TagManagement component for handling tags. - Integrated Redux for state management with auth and settings slices. - Updated package.json to include @reduxjs/toolkit and react-redux dependencies. - Refactored existing components to utilize new DatasetFileTransfer and TagManagement components. - Implemented hooks for typed dispatch and selector in Redux. - Enhanced CreateKnowledgeBase and SynthesisTask components to support new features.
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
||||
import { DatasetFileCols as fileCols } from "../pages/KnowledgeBase/knowledge-base.const";
|
||||
|
||||
interface DatasetFileTransferProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -22,6 +21,28 @@ interface DatasetFileTransferProps
|
||||
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
|
||||
}
|
||||
|
||||
const fileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
// Customize Table Transfer
|
||||
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
|
||||
open,
|
||||
@@ -5,14 +5,18 @@ import router from "./routes/routes";
|
||||
import { App as AntdApp, Spin } from "antd";
|
||||
import "./index.css";
|
||||
import TopLoadingBar from "./components/TopLoadingBar";
|
||||
import { store } from "./store";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<AntdApp>
|
||||
<Suspense fallback={<Spin />}>
|
||||
<TopLoadingBar />
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</AntdApp>
|
||||
<Provider store={store}>
|
||||
<AntdApp>
|
||||
<Suspense fallback={<Spin />}>
|
||||
<TopLoadingBar />
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</AntdApp>
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import TagManager from "@/components/TagManagement";
|
||||
import TagManager from "@/components/business/TagManagement";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import DatasetFileTransfer from "../../../components/DatasetFileTransfer";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFileCols } from "../knowledge-base.const";
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: Object.values(selectedFilesMap),
|
||||
files: Object.entries(selectedFilesMap),
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Form, Input, message, Modal, Select } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
import {
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
updateKnowledgeBaseByIdUsingPut,
|
||||
} from "../knowledge-base.api";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { showSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
export default function CreateKnowledgeBase({
|
||||
isEdit,
|
||||
@@ -25,6 +27,7 @@ export default function CreateKnowledgeBase({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const embeddingModelOptions = models
|
||||
.filter((model) => model.type === "EMBEDDING")
|
||||
@@ -130,6 +133,19 @@ export default function CreateKnowledgeBase({
|
||||
placeholder="请选择索引模型"
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit} // 编辑模式下禁用索引模型修改
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -146,4 +162,4 @@ export default function CreateKnowledgeBase({
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { menuItems } from "@/pages/Layout/menu";
|
||||
import { NavLink, useLocation, useNavigate } from "react-router";
|
||||
import TaskUpload from "./TaskUpload";
|
||||
import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
const AsiderAndHeaderLayout = () => {
|
||||
const { pathname } = useLocation();
|
||||
@@ -17,7 +19,8 @@ const AsiderAndHeaderLayout = () => {
|
||||
const [activeItem, setActiveItem] = useState<string>("");
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const [settingVisible, setSettingVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = () => {
|
||||
@@ -140,7 +143,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
setSettingVisible(true);
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
设置
|
||||
@@ -167,7 +170,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
setSettingVisible(true);
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
@@ -181,7 +184,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
width="100%"
|
||||
height="100%"
|
||||
open={settingVisible}
|
||||
onClose={() => setSettingVisible(false)}
|
||||
onClose={() => dispatch(hideSettings())}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
OperatorI,
|
||||
} from "@/pages/OperatorMarket/operator.model";
|
||||
import Filters from "./components/Filters";
|
||||
import TagManagement from "@/components/TagManagement";
|
||||
import TagManagement from "@/components/business/TagManagement";
|
||||
import { ListView } from "./components/List";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { DatasetFile } from "@/pages/DataManagement/dataset.model";
|
||||
import {
|
||||
Steps,
|
||||
Card,
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { queryDatasetsUsingGet } from "../DataManagement/dataset.api";
|
||||
import DatasetFileTransfer from "../../components/DatasetFileTransfer";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function SynthesisTaskCreate() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [createStep, setCreateStep] = useState(1);
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
||||
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile>>(
|
||||
{}
|
||||
);
|
||||
const [files] = useState<File[]>([]);
|
||||
@@ -318,8 +318,8 @@ export default function SynthesisTaskCreate() {
|
||||
</Form.Item>
|
||||
<DatasetFileTransfer
|
||||
open
|
||||
selectedMap={selectedMap}
|
||||
onSelectedChange={setSelectedMap}
|
||||
selectedFilesMap={selectedMap}
|
||||
onSelectedFilesChange={setSelectedMap}
|
||||
/>
|
||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
||||
任务配置
|
||||
|
||||
6
frontend/src/store/hooks.ts
Normal file
6
frontend/src/store/hooks.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './index';
|
||||
|
||||
// 类型化的 hooks
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
15
frontend/src/store/index.ts
Normal file
15
frontend/src/store/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import authSlice from "./slices/authSlice";
|
||||
import settingsSlice from "./slices/settingsSlice";
|
||||
|
||||
// 创建 Store
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authSlice,
|
||||
settings: settingsSlice,
|
||||
},
|
||||
});
|
||||
|
||||
// 导出类型
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
75
frontend/src/store/slices/authSlice.ts
Normal file
75
frontend/src/store/slices/authSlice.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// store/slices/authSlice.js
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
// 异步 thunk
|
||||
export const loginUser = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (credentials, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: {
|
||||
user: null,
|
||||
token: localStorage.getItem('token'),
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
state.isAuthenticated = false;
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setToken: (state, action) => {
|
||||
state.token = action.payload;
|
||||
localStorage.setItem('token', action.payload);
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(loginUser.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
state.isAuthenticated = true;
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
})
|
||||
.addCase(loginUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
state.isAuthenticated = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout, clearError, setToken } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
23
frontend/src/store/slices/settingsSlice.ts
Normal file
23
frontend/src/store/slices/settingsSlice.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Settings Slice
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState: {
|
||||
visible: false,
|
||||
},
|
||||
reducers: {
|
||||
showSettings: (state) => {
|
||||
state.visible = true;
|
||||
},
|
||||
hideSettings: (state) => {
|
||||
state.visible = false;
|
||||
},
|
||||
toggleSettings: (state) => {
|
||||
state.visible = !state.visible;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { showSettings, hideSettings, toggleSettings } = settingsSlice.actions;
|
||||
export default settingsSlice.reducer;
|
||||
Reference in New Issue
Block a user