mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 01:23:14 +00:00
refactor: UI enhancement in tools
This commit is contained in:
@@ -185,8 +185,13 @@ class SubmitFeedback(Resource):
|
||||
),
|
||||
"answer": fields.String(required=False, description="The AI answer"),
|
||||
"feedback": fields.String(required=True, description="User feedback"),
|
||||
"question_index":fields.Integer(required=True, description="The question number in that particular conversation"),
|
||||
"conversation_id":fields.String(required=True, description="id of the particular conversation"),
|
||||
"question_index": fields.Integer(
|
||||
required=True,
|
||||
description="The question number in that particular conversation",
|
||||
),
|
||||
"conversation_id": fields.String(
|
||||
required=True, description="id of the particular conversation"
|
||||
),
|
||||
"api_key": fields.String(description="Optional API key"),
|
||||
},
|
||||
)
|
||||
@@ -196,21 +201,24 @@ class SubmitFeedback(Resource):
|
||||
)
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = [ "feedback","conversation_id","question_index"]
|
||||
required_fields = ["feedback", "conversation_id", "question_index"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
try:
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(data["conversation_id"]), f"queries.{data['question_index']}": {"$exists": True}},
|
||||
{
|
||||
"$set": {
|
||||
f"queries.{data['question_index']}.feedback": data["feedback"]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
{
|
||||
"_id": ObjectId(data["conversation_id"]),
|
||||
f"queries.{data['question_index']}": {"$exists": True},
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
f"queries.{data['question_index']}.feedback": data["feedback"]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||
|
||||
@@ -253,11 +261,9 @@ class DeleteOldIndexes(Resource):
|
||||
jsonify({"success": False, "message": "Missing required fields"}), 400
|
||||
)
|
||||
|
||||
doc = sources_collection.find_one(
|
||||
{"_id": ObjectId(source_id), "user": "local"}
|
||||
)
|
||||
doc = sources_collection.find_one({"_id": ObjectId(source_id), "user": "local"})
|
||||
if not doc:
|
||||
return make_response(jsonify({"status": "not found"}), 404)
|
||||
return make_response(jsonify({"status": "not found"}), 404)
|
||||
try:
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
shutil.rmtree(os.path.join(current_dir, "indexes", str(doc["_id"])))
|
||||
@@ -271,7 +277,7 @@ class DeleteOldIndexes(Resource):
|
||||
pass
|
||||
except Exception as err:
|
||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||
|
||||
|
||||
sources_collection.delete_one({"_id": ObjectId(source_id)})
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
@@ -498,7 +504,9 @@ class PaginatedSources(Resource):
|
||||
|
||||
total_documents = sources_collection.count_documents(query)
|
||||
total_pages = max(1, math.ceil(total_documents / rows_per_page))
|
||||
page = min(max(1, page), total_pages) # add this to make sure page inbound is within the range
|
||||
page = min(
|
||||
max(1, page), total_pages
|
||||
) # add this to make sure page inbound is within the range
|
||||
sort_order = 1 if sort_order == "asc" else -1
|
||||
skip = (page - 1) * rows_per_page
|
||||
|
||||
@@ -2098,4 +2106,3 @@ class DeleteTool(Resource):
|
||||
return {"success": False, "error": str(err)}, 400
|
||||
|
||||
return {"success": True}, 200
|
||||
|
||||
@@ -23,6 +23,7 @@ const endpoints = {
|
||||
CREATE_TOOL: '/api/create_tool',
|
||||
UPDATE_TOOL_STATUS: '/api/update_tool_status',
|
||||
UPDATE_TOOL: '/api/update_tool',
|
||||
DELETE_TOOL: '/api/delete_tool',
|
||||
},
|
||||
CONVERSATION: {
|
||||
ANSWER: '/api/answer',
|
||||
|
||||
@@ -45,6 +45,8 @@ const userService = {
|
||||
apiClient.post(endpoints.USER.UPDATE_TOOL_STATUS, data),
|
||||
updateTool: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.UPDATE_TOOL, data),
|
||||
deleteTool: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.DELETE_TOOL, data),
|
||||
};
|
||||
|
||||
export default userService;
|
||||
|
||||
15
frontend/src/assets/no-files-dark.svg
Normal file
15
frontend/src/assets/no-files-dark.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="113" height="124" viewBox="0 0 113 124" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="55.5" cy="71" r="53" fill="#2D2E33"/>
|
||||
<rect x="-0.599797" y="0.654564" width="43.9445" height="61.5222" rx="4.39444" transform="matrix(-0.999048 0.0436194 0.0436194 0.999048 68.9873 43.3176)" fill="#45464D" stroke="#5F6167" stroke-width="1.25556"/>
|
||||
<rect x="0.704349" y="-0.540466" width="46.4556" height="64.0333" rx="5.65" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 96.3673 40.893)" fill="#3F4147" stroke="#5F6167" stroke-width="1.25556"/>
|
||||
<path d="M94.3796 45.7849C94.7417 43.0349 92.8059 40.5122 90.0559 40.1501L55.2011 35.5614C52.4511 35.1994 49.9284 37.1352 49.5663 39.8851L48.3372 49.2212L93.1505 55.121L94.3796 45.7849Z" fill="#54555B"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.130526 0.991445 0.991445 0.130526 40.4766 36.7888)" fill="#5F6167"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 57.6758 26.3892)" fill="#5F6167"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.793353 0.608761 0.608761 0.793353 46.6406 28.1023)" fill="#5F6167"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 88.4668 57.0371)" fill="#5F6167"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 86.8281 69.4851)" fill="#5F6167"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 85.1895 81.9333)" fill="#5F6167"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 87.8105 62.0164)" fill="#5F6167"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 86.1719 74.4644)" fill="#5F6167"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 84.5332 86.9126)" fill="#5F6167"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
15
frontend/src/assets/no-files.svg
Normal file
15
frontend/src/assets/no-files.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="113" height="124" viewBox="0 0 113 124" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="55.5" cy="71" r="53" fill="#F1F1F1" fill-opacity="0.5"/>
|
||||
<rect x="-0.599797" y="0.654564" width="43.9445" height="61.5222" rx="4.39444" transform="matrix(-0.999048 0.0436194 0.0436194 0.999048 68.9873 43.3176)" fill="#EEEEEE" stroke="#999999" stroke-width="1.25556"/>
|
||||
<rect x="0.704349" y="-0.540466" width="46.4556" height="64.0333" rx="5.65" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 96.3673 40.893)" fill="#FAFAFA" stroke="#999999" stroke-width="1.25556"/>
|
||||
<path d="M94.3796 45.7849C94.7417 43.0349 92.8059 40.5122 90.0559 40.1501L55.2011 35.5614C52.4511 35.1994 49.9284 37.1352 49.5663 39.8851L48.3372 49.2212L93.1505 55.121L94.3796 45.7849Z" fill="#EEEEEE"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.130526 0.991445 0.991445 0.130526 40.4766 36.7888)" fill="#999999"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 57.6758 26.3892)" fill="#999999"/>
|
||||
<rect width="1.88333" height="6.27778" rx="0.941667" transform="matrix(-0.793353 0.608761 0.608761 0.793353 46.6406 28.1023)" fill="#999999"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 88.4668 57.0371)" fill="#DCDCDC"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 86.8281 69.4851)" fill="#DCDCDC"/>
|
||||
<rect width="36.4111" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 85.1895 81.9333)" fill="#DCDCDC"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 87.8105 62.0164)" fill="#EEEEEE"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 86.1719 74.4644)" fill="#EEEEEE"/>
|
||||
<rect width="13.1833" height="2.51111" rx="1.25556" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 84.5332 86.9126)" fill="#EEEEEE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -84,7 +84,7 @@ export default function AddToolModal({
|
||||
<h2 className="font-semibold text-xl text-jet dark:text-bright-gray px-3">
|
||||
Select a tool to set up
|
||||
</h2>
|
||||
<div className="mt-5 grid grid-cols-3 gap-4 h-[73vh] overflow-auto px-3 py-px">
|
||||
<div className="mt-5 flex flex-col sm:grid sm:grid-cols-3 gap-4 h-[73vh] overflow-auto px-3 py-px">
|
||||
{availableTools.map((tool, index) => (
|
||||
<div
|
||||
role="button"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActiveState } from '../models/misc';
|
||||
import WrapperModal from './WrapperModal';
|
||||
function ConfirmationModal({
|
||||
|
||||
export default function ConfirmationModal({
|
||||
message,
|
||||
modalState,
|
||||
setModalState,
|
||||
@@ -59,5 +61,3 @@ function ConfirmationModal({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmationModal;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { WrapperModalProps } from './types';
|
||||
import Exit from '../assets/exit.svg';
|
||||
|
||||
const WrapperModal: React.FC<WrapperModalProps> = ({
|
||||
import Exit from '../assets/exit.svg';
|
||||
import { WrapperModalProps } from './types';
|
||||
|
||||
export default function WrapperModal({
|
||||
children,
|
||||
close,
|
||||
isPerformingTask,
|
||||
}) => {
|
||||
}: WrapperModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,10 +40,13 @@ const WrapperModal: React.FC<WrapperModalProps> = ({
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]"
|
||||
className="relative w-11/12 rounded-2xl bg-white dark:bg-outer-space sm:w-[512px]"
|
||||
>
|
||||
{!isPerformingTask && (
|
||||
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
|
||||
<button
|
||||
className="absolute top-3 right-4 m-2 w-3 z-50"
|
||||
onClick={close}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
)}
|
||||
@@ -50,6 +54,4 @@ const WrapperModal: React.FC<WrapperModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrapperModal;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ export default function ToolConfig({
|
||||
handleGoBack();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
userService.deleteTool({ id: tool.id }).then(() => {
|
||||
handleGoBack();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="mt-8 flex flex-col gap-4">
|
||||
<div className="mb-4 flex items-center gap-3 text-eerie-black dark:text-bright-gray text-sm">
|
||||
@@ -83,7 +89,7 @@ export default function ToolConfig({
|
||||
Authentication
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<div className="flex mt-4 flex-col sm:flex-row items-start sm:items-center gap-2">
|
||||
{Object.keys(tool?.config).length !== 0 && (
|
||||
<div className="relative w-96">
|
||||
<span className="absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
|
||||
@@ -98,12 +104,20 @@ export default function ToolConfig({
|
||||
></Input>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="rounded-full h-10 w-36 bg-purple-30 text-white hover:bg-[#6F3FD1] text-nowrap text-sm"
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-full px-5 py-[10px] bg-purple-30 text-white hover:bg-[#6F3FD1] text-nowrap text-sm"
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
<button
|
||||
className="rounded-full px-5 py-[10px] border border-solid border-red-500 text-red-500 hover:bg-red-500 hover:text-white text-nowrap text-sm"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -118,7 +132,7 @@ export default function ToolConfig({
|
||||
key={actionIndex}
|
||||
className="w-full border border-silver dark:border-silver/40 rounded-xl"
|
||||
>
|
||||
<div className="h-10 bg-[#F9F9F9] dark:bg-[#28292D] rounded-t-xl border-b border-silver dark:border-silver/40 flex items-center justify-between px-5">
|
||||
<div className="h-10 bg-[#F9F9F9] dark:bg-[#28292D] rounded-t-xl border-b border-silver dark:border-silver/40 flex items-center justify-between px-5 flex-wrap">
|
||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||
{action.name}
|
||||
</p>
|
||||
@@ -146,7 +160,7 @@ export default function ToolConfig({
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-5 relative px-5 w-96">
|
||||
<div className="mt-5 relative px-5 w-full sm:w-96">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter description"
|
||||
|
||||
@@ -2,13 +2,17 @@ import React from 'react';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import CogwheelIcon from '../assets/cogwheel.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import Input from '../components/Input';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import AddToolModal from '../modals/AddToolModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { UserTool } from './types';
|
||||
import ToolConfig from './ToolConfig';
|
||||
import { UserTool } from './types';
|
||||
|
||||
export default function Tools() {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [addToolModalState, setAddToolModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
@@ -86,62 +90,77 @@ export default function Tools() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<img
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-3 right-3 cursor-pointer"
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
>
|
||||
{userTools.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
).length === 0 ? (
|
||||
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
|
||||
<img
|
||||
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||
alt="No tools found"
|
||||
className="h-24 w-24 mx-auto mb-2"
|
||||
/>
|
||||
No tools found
|
||||
</div>
|
||||
) : (
|
||||
userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<img
|
||||
src={CogwheelIcon}
|
||||
alt="settings"
|
||||
className="h-[19px] w-[19px]"
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="absolute top-3 right-3 cursor-pointer"
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
>
|
||||
<img
|
||||
src={CogwheelIcon}
|
||||
alt="settings"
|
||||
className="h-[19px] w-[19px]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[9px]">
|
||||
<p className="text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed">
|
||||
{tool.displayName}
|
||||
</p>
|
||||
<p className="mt-1 h-16 overflow-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed pr-1">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[9px]">
|
||||
<p className="text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed">
|
||||
{tool.displayName}
|
||||
</p>
|
||||
<p className="mt-1 h-16 overflow-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed pr-1">
|
||||
{tool.description}
|
||||
</p>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<label
|
||||
htmlFor={`toolToggle-${index}`}
|
||||
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`toolToggle-${index}`}
|
||||
className="peer sr-only"
|
||||
checked={tool.status}
|
||||
onChange={() =>
|
||||
updateToolStatus(tool.id, !tool.status)
|
||||
}
|
||||
/>
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<label
|
||||
htmlFor={`toolToggle-${index}`}
|
||||
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`toolToggle-${index}`}
|
||||
className="peer sr-only"
|
||||
checked={tool.status}
|
||||
onChange={() =>
|
||||
updateToolStatus(tool.id, !tool.status)
|
||||
}
|
||||
/>
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AddToolModal
|
||||
|
||||
Reference in New Issue
Block a user