@samanhappy/mcphub 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/build.yml +51 -0
- package/.github/workflows/release.yml +19 -0
- package/.prettierrc +7 -0
- package/Dockerfile +51 -0
- package/assets/amap-edit.png +0 -0
- package/assets/amap-result.png +0 -0
- package/assets/cherry-mcp.png +0 -0
- package/assets/cursor-mcp.png +0 -0
- package/assets/cursor-query.png +0 -0
- package/assets/cursor-tools.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/dashboard.zh.png +0 -0
- package/assets/group.png +0 -0
- package/assets/group.zh.png +0 -0
- package/assets/market.zh.png +0 -0
- package/assets/wegroup.jpg +0 -0
- package/assets/wegroup.png +0 -0
- package/assets/wexin.png +0 -0
- package/bin/mcphub.js +3 -0
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/doc/intro.md +73 -0
- package/doc/intro2.md +232 -0
- package/entrypoint.sh +10 -0
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +13 -0
- package/frontend/postcss.config.js +6 -0
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/components/AddGroupForm.tsx +132 -0
- package/frontend/src/components/AddServerForm.tsx +90 -0
- package/frontend/src/components/ChangePasswordForm.tsx +158 -0
- package/frontend/src/components/EditGroupForm.tsx +149 -0
- package/frontend/src/components/EditServerForm.tsx +76 -0
- package/frontend/src/components/GroupCard.tsx +143 -0
- package/frontend/src/components/MarketServerCard.tsx +153 -0
- package/frontend/src/components/MarketServerDetail.tsx +297 -0
- package/frontend/src/components/ProtectedRoute.tsx +27 -0
- package/frontend/src/components/ServerCard.tsx +230 -0
- package/frontend/src/components/ServerForm.tsx +276 -0
- package/frontend/src/components/icons/LucideIcons.tsx +14 -0
- package/frontend/src/components/layout/Content.tsx +17 -0
- package/frontend/src/components/layout/Header.tsx +61 -0
- package/frontend/src/components/layout/Sidebar.tsx +98 -0
- package/frontend/src/components/ui/Badge.tsx +33 -0
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
- package/frontend/src/components/ui/Pagination.tsx +128 -0
- package/frontend/src/components/ui/Toast.tsx +96 -0
- package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
- package/frontend/src/components/ui/ToolCard.tsx +38 -0
- package/frontend/src/contexts/AuthContext.tsx +159 -0
- package/frontend/src/contexts/ToastContext.tsx +60 -0
- package/frontend/src/hooks/useGroupData.ts +232 -0
- package/frontend/src/hooks/useMarketData.ts +410 -0
- package/frontend/src/hooks/useServerData.ts +306 -0
- package/frontend/src/hooks/useSettingsData.ts +131 -0
- package/frontend/src/i18n.ts +42 -0
- package/frontend/src/index.css +20 -0
- package/frontend/src/layouts/MainLayout.tsx +33 -0
- package/frontend/src/locales/en.json +214 -0
- package/frontend/src/locales/zh.json +214 -0
- package/frontend/src/main.tsx +12 -0
- package/frontend/src/pages/Dashboard.tsx +206 -0
- package/frontend/src/pages/GroupsPage.tsx +116 -0
- package/frontend/src/pages/LoginPage.tsx +104 -0
- package/frontend/src/pages/MarketPage.tsx +356 -0
- package/frontend/src/pages/ServersPage.tsx +144 -0
- package/frontend/src/pages/SettingsPage.tsx +149 -0
- package/frontend/src/services/authService.ts +141 -0
- package/frontend/src/types/index.ts +160 -0
- package/frontend/src/utils/cn.ts +10 -0
- package/frontend/tsconfig.json +31 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +26 -0
- package/googled76ca578b6543fbc.html +1 -0
- package/jest.config.js +10 -0
- package/mcp_settings.json +45 -0
- package/package.json +5 -8
- package/servers.json +74722 -0
- package/src/config/index.ts +46 -0
- package/src/controllers/authController.ts +179 -0
- package/src/controllers/groupController.ts +341 -0
- package/src/controllers/marketController.ts +154 -0
- package/src/controllers/serverController.ts +303 -0
- package/src/index.ts +17 -0
- package/src/middlewares/auth.ts +28 -0
- package/src/middlewares/index.ts +43 -0
- package/src/models/User.ts +103 -0
- package/src/routes/index.ts +96 -0
- package/src/server.ts +72 -0
- package/src/services/groupService.ts +232 -0
- package/src/services/marketService.ts +116 -0
- package/src/services/mcpService.ts +385 -0
- package/src/services/sseService.ts +119 -0
- package/src/types/index.ts +129 -0
- package/src/utils/migration.ts +52 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { NavLink, useLocation } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
interface SidebarProps {
|
|
6
|
+
collapsed: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MenuItem {
|
|
10
|
+
path: string;
|
|
11
|
+
label: string;
|
|
12
|
+
icon: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const location = useLocation();
|
|
18
|
+
|
|
19
|
+
// Menu item configuration
|
|
20
|
+
const menuItems: MenuItem[] = [
|
|
21
|
+
{
|
|
22
|
+
path: '/',
|
|
23
|
+
label: t('nav.dashboard'),
|
|
24
|
+
icon: (
|
|
25
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
26
|
+
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
|
|
27
|
+
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
|
|
28
|
+
</svg>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: '/servers',
|
|
33
|
+
label: t('nav.servers'),
|
|
34
|
+
icon: (
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
36
|
+
<path fillRule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clipRule="evenodd" />
|
|
37
|
+
</svg>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: '/groups',
|
|
42
|
+
label: t('nav.groups'),
|
|
43
|
+
icon: (
|
|
44
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
45
|
+
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
|
|
46
|
+
</svg>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: '/market',
|
|
51
|
+
label: t('nav.market'),
|
|
52
|
+
icon: (
|
|
53
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
54
|
+
<path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" />
|
|
55
|
+
</svg>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: '/settings',
|
|
60
|
+
label: t('nav.settings'),
|
|
61
|
+
icon: (
|
|
62
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
63
|
+
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
|
64
|
+
</svg>
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<aside
|
|
71
|
+
className={`bg-white shadow-sm transition-all duration-300 ease-in-out ${
|
|
72
|
+
collapsed ? 'w-16' : 'w-64'
|
|
73
|
+
}`}
|
|
74
|
+
>
|
|
75
|
+
<nav className="p-3 space-y-1">
|
|
76
|
+
{menuItems.map((item) => (
|
|
77
|
+
<NavLink
|
|
78
|
+
key={item.path}
|
|
79
|
+
to={item.path}
|
|
80
|
+
className={({ isActive }) =>
|
|
81
|
+
`flex items-center px-3 py-2 rounded-md transition-colors ${
|
|
82
|
+
isActive
|
|
83
|
+
? 'bg-blue-100 text-blue-800'
|
|
84
|
+
: 'text-gray-700 hover:bg-gray-100'
|
|
85
|
+
}`
|
|
86
|
+
}
|
|
87
|
+
end={item.path === '/'}
|
|
88
|
+
>
|
|
89
|
+
<span className="flex-shrink-0">{item.icon}</span>
|
|
90
|
+
{!collapsed && <span className="ml-3">{item.label}</span>}
|
|
91
|
+
</NavLink>
|
|
92
|
+
))}
|
|
93
|
+
</nav>
|
|
94
|
+
</aside>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default Sidebar;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
2
|
+
import { ServerStatus } from '@/types'
|
|
3
|
+
|
|
4
|
+
interface BadgeProps {
|
|
5
|
+
status: ServerStatus
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Badge = ({ status }: BadgeProps) => {
|
|
9
|
+
const { t } = useTranslation()
|
|
10
|
+
|
|
11
|
+
const colors = {
|
|
12
|
+
connecting: 'bg-yellow-100 text-yellow-800',
|
|
13
|
+
connected: 'bg-green-100 text-green-800',
|
|
14
|
+
disconnected: 'bg-red-100 text-red-800',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Map status to translation keys
|
|
18
|
+
const statusTranslations = {
|
|
19
|
+
connected: 'status.online',
|
|
20
|
+
disconnected: 'status.offline',
|
|
21
|
+
connecting: 'status.connecting'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${colors[status]}`}
|
|
27
|
+
>
|
|
28
|
+
{t(statusTranslations[status] || status)}
|
|
29
|
+
</span>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default Badge
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
2
|
+
|
|
3
|
+
interface DeleteDialogProps {
|
|
4
|
+
isOpen: boolean
|
|
5
|
+
onClose: () => void
|
|
6
|
+
onConfirm: () => void
|
|
7
|
+
serverName: string
|
|
8
|
+
isGroup?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false }: DeleteDialogProps) => {
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
if (!isOpen) return null
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="fixed inset-0 bg-black bg-opacity-30 z-50 flex items-center justify-center p-4">
|
|
18
|
+
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
|
19
|
+
<div className="p-6">
|
|
20
|
+
<h3 className="text-lg font-medium text-gray-900 mb-3">
|
|
21
|
+
{isGroup ? t('groups.confirmDelete') : t('server.confirmDelete')}
|
|
22
|
+
</h3>
|
|
23
|
+
<p className="text-gray-500 mb-6">
|
|
24
|
+
{isGroup
|
|
25
|
+
? t('groups.deleteWarning', { name: serverName })
|
|
26
|
+
: t('server.deleteWarning', { name: serverName })}
|
|
27
|
+
</p>
|
|
28
|
+
<div className="flex justify-end space-x-3">
|
|
29
|
+
<button
|
|
30
|
+
onClick={onClose}
|
|
31
|
+
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
|
32
|
+
>
|
|
33
|
+
{t('common.cancel')}
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
onClick={onConfirm}
|
|
37
|
+
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
|
38
|
+
>
|
|
39
|
+
{t('common.delete')}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default DeleteDialog
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PaginationProps {
|
|
4
|
+
currentPage: number;
|
|
5
|
+
totalPages: number;
|
|
6
|
+
onPageChange: (page: number) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Pagination: React.FC<PaginationProps> = ({
|
|
10
|
+
currentPage,
|
|
11
|
+
totalPages,
|
|
12
|
+
onPageChange
|
|
13
|
+
}) => {
|
|
14
|
+
// Generate page buttons
|
|
15
|
+
const getPageButtons = () => {
|
|
16
|
+
const buttons = [];
|
|
17
|
+
const maxDisplayedPages = 5; // Maximum number of page buttons to display
|
|
18
|
+
|
|
19
|
+
// Always display first page
|
|
20
|
+
buttons.push(
|
|
21
|
+
<button
|
|
22
|
+
key="first"
|
|
23
|
+
onClick={() => onPageChange(1)}
|
|
24
|
+
className={`px-3 py-1 mx-1 rounded ${
|
|
25
|
+
currentPage === 1
|
|
26
|
+
? 'bg-blue-500 text-white'
|
|
27
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
28
|
+
}`}
|
|
29
|
+
>
|
|
30
|
+
1
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Start range
|
|
35
|
+
let startPage = Math.max(2, currentPage - Math.floor(maxDisplayedPages / 2));
|
|
36
|
+
|
|
37
|
+
// If we're showing ellipsis after first page
|
|
38
|
+
if (startPage > 2) {
|
|
39
|
+
buttons.push(
|
|
40
|
+
<span key="ellipsis1" className="px-3 py-1">
|
|
41
|
+
...
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Middle pages
|
|
47
|
+
for (let i = startPage; i <= Math.min(totalPages - 1, startPage + maxDisplayedPages - 3); i++) {
|
|
48
|
+
buttons.push(
|
|
49
|
+
<button
|
|
50
|
+
key={i}
|
|
51
|
+
onClick={() => onPageChange(i)}
|
|
52
|
+
className={`px-3 py-1 mx-1 rounded ${
|
|
53
|
+
currentPage === i
|
|
54
|
+
? 'bg-blue-500 text-white'
|
|
55
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
56
|
+
}`}
|
|
57
|
+
>
|
|
58
|
+
{i}
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If we're showing ellipsis before last page
|
|
64
|
+
if (startPage + maxDisplayedPages - 3 < totalPages - 1) {
|
|
65
|
+
buttons.push(
|
|
66
|
+
<span key="ellipsis2" className="px-3 py-1">
|
|
67
|
+
...
|
|
68
|
+
</span>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Always display last page if there's more than one page
|
|
73
|
+
if (totalPages > 1) {
|
|
74
|
+
buttons.push(
|
|
75
|
+
<button
|
|
76
|
+
key="last"
|
|
77
|
+
onClick={() => onPageChange(totalPages)}
|
|
78
|
+
className={`px-3 py-1 mx-1 rounded ${
|
|
79
|
+
currentPage === totalPages
|
|
80
|
+
? 'bg-blue-500 text-white'
|
|
81
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
82
|
+
}`}
|
|
83
|
+
>
|
|
84
|
+
{totalPages}
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return buttons;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// If there's only one page, don't render pagination
|
|
93
|
+
if (totalPages <= 1) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex justify-center items-center my-6">
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
|
101
|
+
disabled={currentPage === 1}
|
|
102
|
+
className={`px-3 py-1 rounded mr-2 ${
|
|
103
|
+
currentPage === 1
|
|
104
|
+
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
|
105
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
« Prev
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
<div className="flex">{getPageButtons()}</div>
|
|
112
|
+
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
|
115
|
+
disabled={currentPage === totalPages}
|
|
116
|
+
className={`px-3 py-1 rounded ml-2 ${
|
|
117
|
+
currentPage === totalPages
|
|
118
|
+
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
|
119
|
+
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
|
120
|
+
}`}
|
|
121
|
+
>
|
|
122
|
+
Next »
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default Pagination;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Check, X } from 'lucide-react';
|
|
3
|
+
import { cn } from '@/utils/cn';
|
|
4
|
+
|
|
5
|
+
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
6
|
+
|
|
7
|
+
export interface ToastProps {
|
|
8
|
+
message: string;
|
|
9
|
+
type?: ToastType;
|
|
10
|
+
duration?: number;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
visible: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Toast: React.FC<ToastProps> = ({
|
|
16
|
+
message,
|
|
17
|
+
type = 'info',
|
|
18
|
+
duration = 3000,
|
|
19
|
+
onClose,
|
|
20
|
+
visible
|
|
21
|
+
}) => {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (visible) {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
onClose();
|
|
26
|
+
}, duration);
|
|
27
|
+
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}, [visible, duration, onClose]);
|
|
31
|
+
|
|
32
|
+
const icons = {
|
|
33
|
+
success: <Check className="w-5 h-5 text-green-500" />,
|
|
34
|
+
error: <X className="w-5 h-5 text-red-500" />,
|
|
35
|
+
info: (
|
|
36
|
+
<svg className="w-5 h-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
37
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
warning: (
|
|
41
|
+
<svg className="w-5 h-5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
42
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
43
|
+
</svg>
|
|
44
|
+
)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const bgColors = {
|
|
48
|
+
success: 'bg-green-50 border-green-200',
|
|
49
|
+
error: 'bg-red-50 border-red-200',
|
|
50
|
+
info: 'bg-blue-50 border-blue-200',
|
|
51
|
+
warning: 'bg-yellow-50 border-yellow-200'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const textColors = {
|
|
55
|
+
success: 'text-green-800',
|
|
56
|
+
error: 'text-red-800',
|
|
57
|
+
info: 'text-blue-800',
|
|
58
|
+
warning: 'text-yellow-800'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn(
|
|
63
|
+
"fixed top-4 right-4 z-50 max-w-sm p-4 rounded-md shadow-lg border",
|
|
64
|
+
bgColors[type],
|
|
65
|
+
"transform transition-all duration-300 ease-in-out",
|
|
66
|
+
visible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
|
67
|
+
)}>
|
|
68
|
+
<div className="flex items-start">
|
|
69
|
+
<div className="flex-shrink-0">
|
|
70
|
+
{icons[type]}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="ml-3">
|
|
73
|
+
<p className={cn("text-sm font-medium", textColors[type])}>
|
|
74
|
+
{message}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="ml-auto pl-3">
|
|
78
|
+
<div className="-mx-1.5 -my-1.5">
|
|
79
|
+
<button
|
|
80
|
+
onClick={onClose}
|
|
81
|
+
className={cn(
|
|
82
|
+
"inline-flex rounded-md p-1.5",
|
|
83
|
+
`hover:bg-${type}-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-${type}-500`
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<span className="sr-only">Dismiss</span>
|
|
87
|
+
<X className="h-5 w-5" />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default Toast;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
|
|
4
|
+
interface ToggleGroupItemProps {
|
|
5
|
+
value: string;
|
|
6
|
+
isSelected: boolean;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ToggleGroupItem: React.FC<ToggleGroupItemProps> = ({
|
|
12
|
+
value,
|
|
13
|
+
isSelected,
|
|
14
|
+
onClick,
|
|
15
|
+
children
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
role="checkbox"
|
|
21
|
+
aria-checked={isSelected}
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex w-full items-center justify-between p-2 rounded transition-colors cursor-pointer",
|
|
24
|
+
isSelected
|
|
25
|
+
? "bg-blue-50 text-blue-700 hover:bg-blue-100 border-l-4 border-blue-500"
|
|
26
|
+
: "hover:bg-gray-50 text-gray-700"
|
|
27
|
+
)}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
<span className="flex items-center">
|
|
31
|
+
{children}
|
|
32
|
+
</span>
|
|
33
|
+
{isSelected && (
|
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5 text-blue-500">
|
|
35
|
+
<path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
|
|
36
|
+
</svg>
|
|
37
|
+
)}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface ToggleGroupProps {
|
|
43
|
+
label: string;
|
|
44
|
+
helpText?: string;
|
|
45
|
+
noOptionsText?: string;
|
|
46
|
+
values: string[];
|
|
47
|
+
options: { value: string; label: string }[];
|
|
48
|
+
onChange: (values: string[]) => void;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const ToggleGroup: React.FC<ToggleGroupProps> = ({
|
|
53
|
+
label,
|
|
54
|
+
helpText,
|
|
55
|
+
noOptionsText = "No options available",
|
|
56
|
+
values,
|
|
57
|
+
options,
|
|
58
|
+
onChange,
|
|
59
|
+
className
|
|
60
|
+
}) => {
|
|
61
|
+
const handleToggle = (value: string) => {
|
|
62
|
+
const isSelected = values.includes(value);
|
|
63
|
+
if (isSelected) {
|
|
64
|
+
onChange(values.filter(v => v !== value));
|
|
65
|
+
} else {
|
|
66
|
+
onChange([...values, value]);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={className}>
|
|
72
|
+
<label className="block text-gray-700 text-sm font-bold mb-2">
|
|
73
|
+
{label}
|
|
74
|
+
</label>
|
|
75
|
+
<div className="border rounded shadow max-h-60 overflow-y-auto">
|
|
76
|
+
{options.length === 0 ? (
|
|
77
|
+
<p className="text-gray-500 text-sm p-3">{noOptionsText}</p>
|
|
78
|
+
) : (
|
|
79
|
+
<div className="space-y-1 p-1">
|
|
80
|
+
{options.map(option => (
|
|
81
|
+
<ToggleGroupItem
|
|
82
|
+
key={option.value}
|
|
83
|
+
value={option.value}
|
|
84
|
+
isSelected={values.includes(option.value)}
|
|
85
|
+
onClick={() => handleToggle(option.value)}
|
|
86
|
+
>
|
|
87
|
+
{option.label}
|
|
88
|
+
</ToggleGroupItem>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
{helpText && (
|
|
94
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
95
|
+
{helpText}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
interface SwitchProps {
|
|
103
|
+
checked: boolean;
|
|
104
|
+
onCheckedChange: (checked: boolean) => void;
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const Switch: React.FC<SwitchProps> = ({
|
|
109
|
+
checked,
|
|
110
|
+
onCheckedChange,
|
|
111
|
+
disabled = false
|
|
112
|
+
}) => {
|
|
113
|
+
return (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
role="switch"
|
|
117
|
+
aria-checked={checked}
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
className={cn(
|
|
120
|
+
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500",
|
|
121
|
+
checked ? "bg-blue-600" : "bg-gray-200",
|
|
122
|
+
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
|
123
|
+
)}
|
|
124
|
+
onClick={() => !disabled && onCheckedChange(!checked)}
|
|
125
|
+
>
|
|
126
|
+
<span
|
|
127
|
+
className={cn(
|
|
128
|
+
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
|
|
129
|
+
checked ? "translate-x-6" : "translate-x-1"
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
</button>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Tool } from '@/types'
|
|
3
|
+
import { ChevronDown, ChevronRight } from '@/components/icons/LucideIcons'
|
|
4
|
+
|
|
5
|
+
interface ToolCardProps {
|
|
6
|
+
tool: Tool
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ToolCard = ({ tool }: ToolCardProps) => {
|
|
10
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="bg-white shadow rounded-lg p-4 mb-4">
|
|
14
|
+
<div
|
|
15
|
+
className="flex justify-between items-center cursor-pointer"
|
|
16
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
17
|
+
>
|
|
18
|
+
<h3 className="text-lg font-medium text-gray-900">{tool.name}</h3>
|
|
19
|
+
<button className="text-gray-400 hover:text-gray-600">
|
|
20
|
+
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
{isExpanded && (
|
|
24
|
+
<div className="mt-4">
|
|
25
|
+
<p className="text-gray-600 mb-2">{tool.description || 'No description available'}</p>
|
|
26
|
+
<div className="bg-gray-50 rounded p-2">
|
|
27
|
+
<h4 className="text-sm font-medium text-gray-900 mb-2">Input Schema:</h4>
|
|
28
|
+
<pre className="text-xs text-gray-600 overflow-auto">
|
|
29
|
+
{JSON.stringify(tool.inputSchema, null, 2)}
|
|
30
|
+
</pre>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default ToolCard
|