diff --git a/admin/LICENSE b/admin/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/admin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 000000000..f8e6d3d15 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,15 @@ +## fastgpt-admin +原作地址:https://github.com/c121914yu/FastGPT/ + +## 项目原理 +使用tushan项目做前端,然后构造了一个与mongodb做沟通的API做后端,可以做到创建、修改和删除用户 + +## 使用方法 +1. 修改根目录下的server.js文件中的mongodb数据库连接地址。 +2. pnpm i && pnpm dev +3. 默认账号密码为tushan + + +## 可能会有的功能 +1. 对接数据库中的其他数据 +2. tokens充值功能 diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 000000000..87e4826e6 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,13 @@ + + + + + + + Tushan + + +
+ + + diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 000000000..f6b466a6b --- /dev/null +++ b/admin/package.json @@ -0,0 +1,30 @@ +{ + "name": "kbgpt-deafult", + "private": true, + "version": "0.0.0", + "type": "module", + "author": "anonymous", + "scripts": { + "dev": "concurrently \"vite\" \"npm run start:api\"", + "build": "tsc && vite build", + "preview": "vite preview", + "start:api": "node server.js" + }, + "dependencies": { + "concurrently": "^8.1.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "mongoose": "^7.2.2", + "react": "^18.2.0", + "react-admin": "^4.11.0", + "react-dom": "^18.2.0", + "tushan": "^0.2.13" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^3.1.0", + "typescript": "^4.9.3", + "vite": "^4.2.1" + } +} diff --git a/admin/public/logo.svg b/admin/public/logo.svg new file mode 100644 index 000000000..7eabef970 --- /dev/null +++ b/admin/public/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/admin/server.js b/admin/server.js new file mode 100644 index 000000000..b127dd503 --- /dev/null +++ b/admin/server.js @@ -0,0 +1,329 @@ +import express from 'express'; +import mongoose from 'mongoose'; +import cors from 'cors'; + +const app = express(); +app.use(cors()); +app.use(express.json()); + +const mongoURI = '';//在这里填入mongodb的连接地址 +mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(() => console.log('Connected to MongoDB successfully!')) + .catch((err) => console.log(`Error connecting to MongoDB: ${err}`)); + +const userSchema = new mongoose.Schema({ + _id: mongoose.Schema.Types.ObjectId, + username: String, + password: String, + balance: Number, + promotion: { + rate: Number, + }, + openaiKey: String, + avatar: String, + createTime: Date, +}); + +// 新增: 定义 pays 模型 +const paySchema = new mongoose.Schema({ + _id: mongoose.Schema.Types.ObjectId, + userId: mongoose.Schema.Types.ObjectId, + price: Number, + orderId: String, + status: String, + createTime: Date, + __v: Number, +}); + +// 新增: 定义 kb 模型 +const kbSchema = new mongoose.Schema({ + _id: mongoose.Schema.Types.ObjectId, + userId: mongoose.Schema.Types.ObjectId, + avatar: String, + name: String, + tags: [String], + updateTime: Date, + __v: Number, +}); + + +const modelSchema = new mongoose.Schema({ + userId: mongoose.Schema.Types.ObjectId, + name: String, + avatar: String, + status: String, + chat: { + relatedKbs: [mongoose.Schema.Types.ObjectId], + searchMode: String, + systemPrompt: String, + temperature: Number, + chatModel: String + }, + share: { + isShare: Boolean, + isShareDetail: Boolean, + intro: String, + collection: Number + }, + security: { + domain: [String], + contextMaxLen: Number, + contentMaxLen: Number, + expiredTime: Number, + maxLoadAmount: Number + }, + updateTime: Date +}); + + +const Model = mongoose.model('Model', modelSchema); +const Kb = mongoose.model('Kb', kbSchema); +const User = mongoose.model('User', userSchema, 'users'); +const Pay = mongoose.model('Pay', paySchema, 'pays'); + +// 获取用户列表 +app.get('/users', async (req, res) => { + try { + const start = parseInt(req.query._start) || 0; + const end = parseInt(req.query._end) || 20; + const order = req.query._order === 'DESC' ? -1 : 1; + const sort = req.query._sort || '_id'; + + const usersRaw = await User.find() + .skip(start) + .limit(end - start) + .sort({ [sort]: order }); + const users = usersRaw.map((user) => { + const obj = user.toObject(); + obj.id = obj._id; + delete obj._id; + return obj; + }); + + const totalCount = await User.countDocuments(); + + res.header('Access-Control-Expose-Headers', 'X-Total-Count'); + res.header('X-Total-Count', totalCount); + res.json(users); + } catch (err) { + console.log(`Error fetching users: ${err}`); + res.status(500).json({ error: 'Error fetching users' }); + } +}); + +// 创建用户 +app.post('/users', async (req, res) => { + try { + const { username, password, balance, promotion, openaiKey = '', avatar = '/icon/human.png' } = req.body; + if (!username || !password || !balance) { + return res.status(400).json({ error: 'Invalid user information' }); + } + const existingUser = await User.findOne({ username }); + if (existingUser) { + return res.status(400).json({ error: 'Username already exists' }); + } + const user = new User({ + _id: new mongoose.Types.ObjectId(), + username, + password, + balance, + promotion: { + rate: promotion?.rate || 0, + }, + openaiKey, + avatar, + createTime: new Date(), + }); + const result = await user.save(); + res.json(result); + } catch (err) { + console.log(`Error creating user: ${err}`); + res.status(500).json({ error: 'Error creating user' }); + } + }); + + + + + +// 修改用户信息 +app.put('/users/:id', async (req, res) => { + try { + const _id = req.params.id; + + const result = await User.updateOne({ _id: _id }, { $set: req.body }); + res.json(result); + } catch (err) { + console.log(`Error updating user: ${err}`); + res.status(500).json({ error: 'Error updating user' }); + } +}); + +// 删除用户 +app.delete('/users/:id', async (req, res) => { + try { + const _id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(_id)) { + return res.status(400).json({ error: 'Invalid user ID' }); + } + const result = await User.deleteOne({ _id: _id }); + res.json(result); + } catch (err) { + console.log(`Error deleting user: ${err}`); + res.status(500).json({ error: 'Error deleting user' }); + } + }); + +// 新增: 获取 pays 列表 +app.get('/pays', async (req, res) => { + try { + const start = parseInt(req.query._start) || 0; + const end = parseInt(req.query._end) || 20; + const order = req.query._order === 'DESC' ? -1 : 1; + const sort = req.query._sort || '_id'; + + const paysRaw = await Pay.find() + .skip(start) + .limit(end - start) + .sort({ [sort]: order }); + + const usersMap = new Map(); + const pays = []; + + for (const payRaw of paysRaw) { + const pay = payRaw.toObject(); + + if (!usersMap.has(pay.userId.toString())) { + const user = await User.findById(pay.userId); + usersMap.set(pay.userId.toString(), user.username); + } + + const orderedPay = { + id: pay._id.toString(), + name: usersMap.get(pay.userId.toString()), + price: pay.price, + orderId: pay.orderId, + status: pay.status, + createTime: pay.createTime + }; + + pays.push(orderedPay); + } + const totalCount = await Pay.countDocuments(); + res.header('Access-Control-Expose-Headers', 'X-Total-Count'); + res.header('X-Total-Count', totalCount); + res.json(pays); + } catch (err) { + console.log(`Error fetching pays: ${err}`); + res.status(500).json({ error: 'Error fetching pays', details: err.message }); + } +}); + +// 获取用户知识库列表 +app.get('/kbs', async (req, res) => { + try { + const start = parseInt(req.query._start) || 0; + const end = parseInt(req.query._end) || 20; + const order = req.query._order === 'DESC' ? -1 : 1; + const sort = req.query._sort || '_id'; + + const kbsRaw = await Kb.find() + .skip(start) + .limit(end - start) + .sort({ [sort]: order }); + + const usersMap = new Map(); + const kbs = []; + + for (const kbRaw of kbsRaw) { + const kb = kbRaw.toObject(); + + if (!usersMap.has(kb.userId.toString())) { + const user = await User.findById(kb.userId); + usersMap.set(kb.userId.toString(), user.username); + } + + const orderedKb = { + id: kb._id.toString(), + user: usersMap.get(kb.userId.toString()), + name: kb.name, + tags: kb.tags, + avatar: kb.avatar + }; + + kbs.push(orderedKb); + } + const totalCount = await Kb.countDocuments(); + res.header('Access-Control-Expose-Headers', 'X-Total-Count'); + res.header('X-Total-Count', totalCount); + res.json(kbs); + } catch (err) { + console.log(`Error fetching kbs: ${err}`); + res.status(500).json({ error: 'Error fetching kbs', details: err.message }); + } +}); + +// 获取AI助手列表 +app.get('/models', async (req, res) => { + try { + const start = parseInt(req.query._start) || 0; + const end = parseInt(req.query._end) || 20; + const order = req.query._order === 'DESC' ? -1 : 1; + const sort = req.query._sort || '_id'; + + const modelsRaw = await Model.find() + .skip(start) + .limit(end - start) + .sort({ [sort]: order }); + + const usersMap = new Map(); + const models = []; + + for (const modelRaw of modelsRaw) { + const model = modelRaw.toObject(); + + if (!usersMap.has(model.userId.toString())) { + const user = await User.findById(model.userId); + usersMap.set(model.userId.toString(), user.username); + } + + // 获取与模型关联的知识库名称 + const kbNames = []; + for (const kbId of model.chat.relatedKbs) { + const kb = await Kb.findById(kbId); + kbNames.push(kb.name); + } + + const orderedModel = { + id: model._id.toString(), + user: usersMap.get(model.userId.toString()), + name: model.name, + relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称 + searchMode: model.chat.searchMode, + systemPrompt: model.chat.systemPrompt, + temperature: model.chat.temperature, + isShare: model.share.isShare, + isShareDetail: model.share.isShareDetail, + avatar: model.avatar + }; + + models.push(orderedModel); + } + const totalCount = await Model.countDocuments(); + res.header('Access-Control-Expose-Headers', 'X-Total-Count'); + res.header('X-Total-Count', totalCount); + res.json(models); + } catch (err) { + console.log(`Error fetching models: ${err}`); + res.status(500).json({ error: 'Error fetching models', details: err.message }); + } +}); + + + + +const PORT = process.env.PORT || 3001; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); + diff --git a/admin/src/App.tsx b/admin/src/App.tsx new file mode 100644 index 000000000..26b6cbc65 --- /dev/null +++ b/admin/src/App.tsx @@ -0,0 +1,72 @@ +import { + createTextField, + jsonServerProvider, + ListTable, + Resource, + Tushan, +} from 'tushan'; +import { authProvider } from './auth'; +import { userFields,payFields,kbFields,ModelFields } from './fields'; + +const dataProvider = jsonServerProvider('http://localhost:3001'); + +function App() { + return ( + + + } + /> + + + } + /> + + } + /> + + } + /> + + ); +} + +export default App; diff --git a/admin/src/auth.ts b/admin/src/auth.ts new file mode 100644 index 000000000..92658cfe5 --- /dev/null +++ b/admin/src/auth.ts @@ -0,0 +1,33 @@ +import { AuthProvider } from 'tushan'; + +export const authProvider: AuthProvider = { + login: ({ username, password }) => { + if (username !== 'tushan' || password !== 'tushan') { + return Promise.reject(); + } + + localStorage.setItem('username', username); + return Promise.resolve(); + }, + logout: () => { + localStorage.removeItem('username'); + return Promise.resolve(); + }, + checkAuth: () => + localStorage.getItem('username') ? Promise.resolve() : Promise.reject(), + checkError: (error) => { + const status = error.status; + if (status === 401 || status === 403) { + localStorage.removeItem('username'); + return Promise.reject(); + } + + return Promise.resolve(); + }, + getIdentity: () => + Promise.resolve({ + id: '0', + fullName: 'Admin', + }), + getPermissions: () => Promise.resolve(''), +}; diff --git a/admin/src/fields.ts b/admin/src/fields.ts new file mode 100644 index 000000000..8201adb8d --- /dev/null +++ b/admin/src/fields.ts @@ -0,0 +1,46 @@ +import { + createTextField, + createUrlField, + createNumberField, + createAvatarField +} from 'tushan'; + +export const userFields = [ + createTextField('id', { label: 'ID' }), + createTextField('username', { label: '用户名', list: { sort: true } }), + createTextField('password', { label: '密码(加密)' }), + createNumberField('balance', { label: '余额' }), + createTextField('openaiKey', { label: 'OpenAI Key' }), + createTextField('createTime', { label: 'Create Time' }), + createAvatarField('avatar', { label: 'Avatar' }), +]; + +export const payFields = [ + createTextField('id', { label: 'ID' }), + createTextField('name', { label: '用户名', list: { sort: true } }), + createNumberField('price', { label: '支付金额' }), + createTextField('orderId', { label: 'orderId' }), + createTextField('status', { label: '状态' }), + createTextField('createTime', { label: 'Create Time' }), +]; + +export const kbFields = [ + createTextField('id', { label: 'ID' }), + createTextField('user', { label: '所属用户' }), + createTextField('name', { label: '知识库', list: { sort: true } }), + createTextField('tags', { label: 'Tags' }), + createAvatarField('avatar', { label: 'Avatar' }), +]; + +export const ModelFields = [ + createTextField('id', { label: 'ID' }), + createTextField('name', { label: 'Ai助手', list: { sort: true } }), + createTextField('user', { label: '所属用户' }), + createTextField('relatedKbs', { label: '引用的知识库' }), + createTextField('searchMode', { label: '搜索模式' }), + createTextField('systemPrompt', { label: '提示词' }), + createTextField('temperature', { label: '温度' }), + createTextField('isShare', { label: '是否分享' }), + createTextField('isShareDetail', { label: '分享详情' }), + createAvatarField('avatar', { label: 'Avatar' }), +]; \ No newline at end of file diff --git a/admin/src/main.tsx b/admin/src/main.tsx new file mode 100644 index 000000000..f84c7655f --- /dev/null +++ b/admin/src/main.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + +); diff --git a/admin/src/vite-env.d.ts b/admin/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/admin/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/admin/tsconfig.json b/admin/tsconfig.json new file mode 100644 index 000000000..3d0a51a86 --- /dev/null +++ b/admin/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/admin/tsconfig.node.json b/admin/tsconfig.node.json new file mode 100644 index 000000000..9d31e2aed --- /dev/null +++ b/admin/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/admin/vite.config.ts b/admin/vite.config.ts new file mode 100644 index 000000000..627a31962 --- /dev/null +++ b/admin/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +});