@nospt/plugin-dev-ai-hub 0.1.0
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/.eslintrc.js +1 -0
- package/dev/index.tsx +232 -0
- package/dist/api/DevAiHubClient.esm.js +76 -0
- package/dist/api/DevAiHubClient.esm.js.map +1 -0
- package/dist/components/AssetCard/AssetCard.esm.js +194 -0
- package/dist/components/AssetCard/AssetCard.esm.js.map +1 -0
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +324 -0
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -0
- package/dist/components/AssetFilters/AssetFilters.esm.js +216 -0
- package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -0
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +194 -0
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -0
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +320 -0
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -0
- package/dist/components/DevAiHubPage/index.esm.js +6 -0
- package/dist/components/DevAiHubPage/index.esm.js.map +1 -0
- package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +274 -0
- package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +1 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js +41 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -0
- package/dist/hooks/index.esm.js +114 -0
- package/dist/hooks/index.esm.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +40 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/pluginLegacy.esm.js +27 -0
- package/dist/pluginLegacy.esm.js.map +1 -0
- package/dist/routes.esm.js +6 -0
- package/dist/routes.esm.js.map +1 -0
- package/package.json +80 -0
- package/package.json-prepack +75 -0
- package/src/api/DevAiHubClient.ts +110 -0
- package/src/components/AssetCard/AssetCard.test.tsx +182 -0
- package/src/components/AssetCard/AssetCard.tsx +221 -0
- package/src/components/AssetCard/index.ts +1 -0
- package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +377 -0
- package/src/components/AssetDetailPanel/index.ts +1 -0
- package/src/components/AssetFilters/AssetFilters.tsx +261 -0
- package/src/components/AssetFilters/index.ts +2 -0
- package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +216 -0
- package/src/components/AssetInstallDialog/index.ts +1 -0
- package/src/components/DevAiHubPage/DevAiHubPage.tsx +346 -0
- package/src/components/DevAiHubPage/index.ts +1 -0
- package/src/components/McpConfigDialog/McpConfigDialog.tsx +301 -0
- package/src/components/McpConfigDialog/index.ts +1 -0
- package/src/components/ToolIcon/ToolIcon.tsx +49 -0
- package/src/components/ToolIcon/index.ts +1 -0
- package/src/hooks/index.ts +152 -0
- package/src/index.ts +10 -0
- package/src/plugin.tsx +47 -0
- package/src/pluginLegacy.ts +42 -0
- package/src/routes.ts +3 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import Button from '@mui/material/Button';
|
|
5
|
+
import Grid from '@mui/material/Grid';
|
|
6
|
+
import Pagination from '@mui/material/Pagination';
|
|
7
|
+
import Skeleton from '@mui/material/Skeleton';
|
|
8
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
9
|
+
import Typography from '@mui/material/Typography';
|
|
10
|
+
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
|
11
|
+
import ArticleIcon from '@mui/icons-material/Article';
|
|
12
|
+
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
|
13
|
+
import BuildIcon from '@mui/icons-material/Build';
|
|
14
|
+
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
|
15
|
+
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
|
16
|
+
import HubIcon from '@mui/icons-material/Hub';
|
|
17
|
+
import { Content, Header, Page } from '@backstage/core-components';
|
|
18
|
+
import type { AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
|
|
19
|
+
import { AssetCard } from '../AssetCard';
|
|
20
|
+
import { AssetFilters } from '../AssetFilters';
|
|
21
|
+
import type { AssetFiltersValue } from '../AssetFilters';
|
|
22
|
+
import { AssetDetailPanel } from '../AssetDetailPanel';
|
|
23
|
+
import { AssetInstallDialog } from '../AssetInstallDialog';
|
|
24
|
+
import { McpConfigDialog } from '../McpConfigDialog';
|
|
25
|
+
import { ToolIcon } from '../ToolIcon';
|
|
26
|
+
import { useAssets, useStats, useProviders } from '../../hooks';
|
|
27
|
+
|
|
28
|
+
const SUPPORTED_TOOLS: AiTool[] = ['claude-code', 'github-copilot', 'google-gemini', 'cursor'];
|
|
29
|
+
|
|
30
|
+
const TOOL_LABELS: Record<AiTool, string> = {
|
|
31
|
+
'all': 'Universal',
|
|
32
|
+
'claude-code': 'Claude Code',
|
|
33
|
+
'github-copilot': 'GitHub Copilot',
|
|
34
|
+
'google-gemini': 'Google Gemini',
|
|
35
|
+
'cursor': 'Cursor',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function timeAgo(iso: string): string {
|
|
39
|
+
const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
40
|
+
if (diff < 60) return 'just now';
|
|
41
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
42
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
43
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_FILTERS: AssetFiltersValue = {
|
|
47
|
+
types: [],
|
|
48
|
+
tools: [],
|
|
49
|
+
search: '',
|
|
50
|
+
tags: [],
|
|
51
|
+
providerId: undefined,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PAGE_SIZE = 24;
|
|
55
|
+
|
|
56
|
+
const STATS_CONFIG = [
|
|
57
|
+
{ key: 'instruction' as AssetType, label: 'Instructions', Icon: ArticleIcon,
|
|
58
|
+
gradient: 'linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%)', shadow: '#2563EB40' },
|
|
59
|
+
{ key: 'agent' as AssetType, label: 'Agents', Icon: SmartToyIcon,
|
|
60
|
+
gradient: 'linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)', shadow: '#7C3AED40' },
|
|
61
|
+
{ key: 'skill' as AssetType, label: 'Skills', Icon: BuildIcon,
|
|
62
|
+
gradient: 'linear-gradient(135deg, #059669 0%, #047857 100%)', shadow: '#05966940' },
|
|
63
|
+
{ key: 'workflow' as AssetType, label: 'Workflows', Icon: AccountTreeIcon,
|
|
64
|
+
gradient: 'linear-gradient(135deg, #D97706 0%, #B45309 100%)', shadow: '#D9770640' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export function DevAiHubPage() {
|
|
68
|
+
const [filters, setFilters] = useState<AssetFiltersValue>(DEFAULT_FILTERS);
|
|
69
|
+
const [page, setPage] = useState(1);
|
|
70
|
+
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
|
71
|
+
|
|
72
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
73
|
+
const selectedAssetId = searchParams.get('assetId');
|
|
74
|
+
const installAssetId = searchParams.get('installId');
|
|
75
|
+
|
|
76
|
+
const handleViewAsset = (id: string) =>
|
|
77
|
+
setSearchParams(p => { const n = new URLSearchParams(p); n.set('assetId', id); return n; });
|
|
78
|
+
|
|
79
|
+
const handleCloseDetail = () =>
|
|
80
|
+
setSearchParams(p => { const n = new URLSearchParams(p); n.delete('assetId'); return n; });
|
|
81
|
+
|
|
82
|
+
const handleInstallAsset = (id: string) =>
|
|
83
|
+
setSearchParams(p => { const n = new URLSearchParams(p); n.set('installId', id); return n; });
|
|
84
|
+
|
|
85
|
+
const handleCloseInstall = () =>
|
|
86
|
+
setSearchParams(p => { const n = new URLSearchParams(p); n.delete('installId'); return n; });
|
|
87
|
+
|
|
88
|
+
const { stats } = useStats();
|
|
89
|
+
const { providers } = useProviders();
|
|
90
|
+
|
|
91
|
+
const apiFilter = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
type: filters.types.length === 1 ? (filters.types[0] as AssetType) : undefined,
|
|
94
|
+
tool: filters.tools.length === 1 ? (filters.tools[0] as AiTool) : undefined,
|
|
95
|
+
search: filters.search || undefined,
|
|
96
|
+
tags: filters.tags.length > 0 ? filters.tags : undefined,
|
|
97
|
+
providerId: filters.providerId || undefined,
|
|
98
|
+
page,
|
|
99
|
+
pageSize: PAGE_SIZE,
|
|
100
|
+
}),
|
|
101
|
+
[filters, page],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const { result, loading } = useAssets(apiFilter);
|
|
105
|
+
|
|
106
|
+
const handleFiltersChange = (next: AssetFiltersValue) => {
|
|
107
|
+
setFilters(next);
|
|
108
|
+
setPage(1);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const totalPages = result ? Math.ceil(result.totalCount / PAGE_SIZE) : 0;
|
|
112
|
+
|
|
113
|
+
const availableTags = useMemo(() => {
|
|
114
|
+
if (!result) return [];
|
|
115
|
+
const tagSet = new Set<string>();
|
|
116
|
+
result.items.forEach(a => a.tags.forEach(t => tagSet.add(t)));
|
|
117
|
+
return Array.from(tagSet).sort();
|
|
118
|
+
}, [result]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Page themeId="tool">
|
|
122
|
+
<Header
|
|
123
|
+
title={
|
|
124
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
125
|
+
<Box
|
|
126
|
+
sx={{
|
|
127
|
+
display: 'flex',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
justifyContent: 'center',
|
|
130
|
+
width: 44,
|
|
131
|
+
height: 44,
|
|
132
|
+
borderRadius: 2.5,
|
|
133
|
+
background: 'rgba(255,255,255,0.15)',
|
|
134
|
+
backdropFilter: 'blur(8px)',
|
|
135
|
+
border: '1px solid rgba(255,255,255,0.3)',
|
|
136
|
+
boxShadow: '0 2px 12px rgba(0,0,0,0.15)',
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<HubIcon sx={{ fontSize: '1.5rem', color: '#fff' }} />
|
|
141
|
+
</Box>
|
|
142
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3 }}>
|
|
143
|
+
<Box sx={{ fontSize: '1.75rem', fontWeight: 700, lineHeight: 1, color: '#fff' }}>
|
|
144
|
+
Dev AI Hub
|
|
145
|
+
</Box>
|
|
146
|
+
<Box sx={{ fontSize: '0.85rem', fontWeight: 400, color: 'rgba(255,255,255,0.75)', lineHeight: 1 }}>
|
|
147
|
+
{stats
|
|
148
|
+
? `${stats.totalAssets} assets · ${Object.keys(stats.byProvider).length} provider${Object.keys(stats.byProvider).length !== 1 ? 's' : ''}`
|
|
149
|
+
: 'Centralized AI assets for your team'}
|
|
150
|
+
</Box>
|
|
151
|
+
</Box>
|
|
152
|
+
</Box>
|
|
153
|
+
}
|
|
154
|
+
pageTitleOverride="Dev AI Hub"
|
|
155
|
+
>
|
|
156
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
157
|
+
{/* Supported tools */}
|
|
158
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
|
159
|
+
{SUPPORTED_TOOLS.map(tool => (
|
|
160
|
+
<Tooltip key={tool} title={TOOL_LABELS[tool]} arrow>
|
|
161
|
+
<Box
|
|
162
|
+
sx={{
|
|
163
|
+
width: 32,
|
|
164
|
+
height: 32,
|
|
165
|
+
borderRadius: '50%',
|
|
166
|
+
backgroundColor: 'rgba(255,255,255,0.15)',
|
|
167
|
+
display: 'flex',
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
justifyContent: 'center',
|
|
170
|
+
backdropFilter: 'blur(4px)',
|
|
171
|
+
transition: 'background-color 0.15s',
|
|
172
|
+
'&:hover': { backgroundColor: 'rgba(255,255,255,0.25)' },
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<ToolIcon tool={tool} branded={false} sx={{ fontSize: '1rem', color: '#fff' }} />
|
|
176
|
+
</Box>
|
|
177
|
+
</Tooltip>
|
|
178
|
+
))}
|
|
179
|
+
</Box>
|
|
180
|
+
|
|
181
|
+
{/* Last sync status */}
|
|
182
|
+
{stats?.lastSync && (
|
|
183
|
+
<Tooltip title={`Last sync: ${new Date(stats.lastSync).toLocaleString()}`} arrow>
|
|
184
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'default' }}>
|
|
185
|
+
<FiberManualRecordIcon sx={{ fontSize: '0.6rem', color: '#4ade80' }} />
|
|
186
|
+
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.8)', whiteSpace: 'nowrap' }}>
|
|
187
|
+
{timeAgo(stats.lastSync)}
|
|
188
|
+
</Typography>
|
|
189
|
+
</Box>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Configure MCP button */}
|
|
194
|
+
<Button
|
|
195
|
+
variant="contained"
|
|
196
|
+
size="small"
|
|
197
|
+
startIcon={<SettingsEthernetIcon />}
|
|
198
|
+
onClick={() => setMcpDialogOpen(true)}
|
|
199
|
+
sx={{
|
|
200
|
+
borderRadius: 2,
|
|
201
|
+
fontWeight: 700,
|
|
202
|
+
whiteSpace: 'nowrap',
|
|
203
|
+
background: 'rgba(255,255,255,0.15)',
|
|
204
|
+
backdropFilter: 'blur(4px)',
|
|
205
|
+
border: '1px solid rgba(255,255,255,0.3)',
|
|
206
|
+
color: '#fff',
|
|
207
|
+
boxShadow: 'none',
|
|
208
|
+
'&:hover': {
|
|
209
|
+
background: 'rgba(255,255,255,0.25)',
|
|
210
|
+
boxShadow: 'none',
|
|
211
|
+
},
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
Configure MCP
|
|
215
|
+
</Button>
|
|
216
|
+
</Box>
|
|
217
|
+
</Header>
|
|
218
|
+
|
|
219
|
+
<Content>
|
|
220
|
+
|
|
221
|
+
{/* Stats row */}
|
|
222
|
+
<Grid container spacing={2} sx={{ mb: 3 }}>
|
|
223
|
+
{STATS_CONFIG.map(({ key, label, Icon, gradient, shadow }) => (
|
|
224
|
+
<Grid item xs={6} sm={3} key={key}>
|
|
225
|
+
<Box
|
|
226
|
+
onClick={() => handleFiltersChange({ ...filters, types: filters.types[0] === key ? [] : [key] })}
|
|
227
|
+
sx={{
|
|
228
|
+
background: gradient,
|
|
229
|
+
borderRadius: 3,
|
|
230
|
+
p: 2,
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
boxShadow: filters.types[0] === key ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
|
|
233
|
+
transform: filters.types[0] === key ? 'translateY(-2px)' : 'none',
|
|
234
|
+
transition: 'all 0.2s ease',
|
|
235
|
+
outline: filters.types[0] === key ? '2px solid rgba(255,255,255,0.6)' : 'none',
|
|
236
|
+
outlineOffset: 2,
|
|
237
|
+
'&:hover': {
|
|
238
|
+
boxShadow: `0 8px 24px ${shadow}`,
|
|
239
|
+
transform: 'translateY(-2px)',
|
|
240
|
+
},
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
244
|
+
<Box>
|
|
245
|
+
<Typography variant="h4" fontWeight={800} sx={{ color: '#fff', lineHeight: 1 }}>
|
|
246
|
+
{stats ? (stats.byType[key] ?? 0) : <Skeleton width={32} sx={{ bgcolor: 'rgba(255,255,255,0.3)' }} />}
|
|
247
|
+
</Typography>
|
|
248
|
+
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)', fontWeight: 500, mt: 0.5 }}>
|
|
249
|
+
{label}
|
|
250
|
+
</Typography>
|
|
251
|
+
</Box>
|
|
252
|
+
<Box
|
|
253
|
+
sx={{
|
|
254
|
+
width: 40,
|
|
255
|
+
height: 40,
|
|
256
|
+
borderRadius: 2,
|
|
257
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
258
|
+
display: 'flex',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'center',
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
<Icon sx={{ color: '#fff', fontSize: '1.4rem' }} />
|
|
264
|
+
</Box>
|
|
265
|
+
</Box>
|
|
266
|
+
</Box>
|
|
267
|
+
</Grid>
|
|
268
|
+
))}
|
|
269
|
+
</Grid>
|
|
270
|
+
|
|
271
|
+
{/* Filters */}
|
|
272
|
+
<AssetFilters
|
|
273
|
+
value={filters}
|
|
274
|
+
onChange={handleFiltersChange}
|
|
275
|
+
availableTags={availableTags}
|
|
276
|
+
providers={providers.length > 1 ? providers : undefined}
|
|
277
|
+
/>
|
|
278
|
+
|
|
279
|
+
{/* Results summary */}
|
|
280
|
+
{result && !loading && (
|
|
281
|
+
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
|
|
282
|
+
{result.totalCount} asset{result.totalCount !== 1 ? 's' : ''} found
|
|
283
|
+
</Typography>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{/* Asset grid — 4 columns on large screens */}
|
|
287
|
+
<Grid container spacing={1.5}>
|
|
288
|
+
{loading
|
|
289
|
+
? Array.from({ length: 8 }).map((_, i) => (
|
|
290
|
+
<Grid item xs={12} sm={6} md={4} lg={3} key={i}>
|
|
291
|
+
<Skeleton variant="rectangular" height={150} sx={{ borderRadius: 2 }} />
|
|
292
|
+
</Grid>
|
|
293
|
+
))
|
|
294
|
+
: result?.items.map(asset => (
|
|
295
|
+
<Grid item xs={12} sm={6} md={4} lg={3} key={asset.id}>
|
|
296
|
+
<AssetCard
|
|
297
|
+
asset={asset}
|
|
298
|
+
onView={handleViewAsset}
|
|
299
|
+
onInstall={handleInstallAsset}
|
|
300
|
+
/>
|
|
301
|
+
</Grid>
|
|
302
|
+
))}
|
|
303
|
+
</Grid>
|
|
304
|
+
|
|
305
|
+
{/* Empty state */}
|
|
306
|
+
{!loading && result?.items.length === 0 && (
|
|
307
|
+
<Box sx={{ py: 12, textAlign: 'center' }}>
|
|
308
|
+
<Typography variant="h1" sx={{ mb: 2, opacity: 0.15, fontSize: '5rem' }}>🤖</Typography>
|
|
309
|
+
<Typography variant="h6" color="text.secondary" fontWeight={600}>No assets found</Typography>
|
|
310
|
+
<Typography variant="body2" color="text.disabled" sx={{ mt: 0.5 }}>
|
|
311
|
+
Try adjusting your filters or search terms.
|
|
312
|
+
</Typography>
|
|
313
|
+
</Box>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Pagination */}
|
|
317
|
+
{totalPages > 1 && (
|
|
318
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
|
319
|
+
<Pagination
|
|
320
|
+
count={totalPages}
|
|
321
|
+
page={page}
|
|
322
|
+
onChange={(_, p) => setPage(p)}
|
|
323
|
+
color="primary"
|
|
324
|
+
shape="rounded"
|
|
325
|
+
/>
|
|
326
|
+
</Box>
|
|
327
|
+
)}
|
|
328
|
+
</Content>
|
|
329
|
+
|
|
330
|
+
<AssetDetailPanel
|
|
331
|
+
assetId={selectedAssetId}
|
|
332
|
+
onClose={handleCloseDetail}
|
|
333
|
+
/>
|
|
334
|
+
|
|
335
|
+
<AssetInstallDialog
|
|
336
|
+
assetId={installAssetId}
|
|
337
|
+
onClose={handleCloseInstall}
|
|
338
|
+
/>
|
|
339
|
+
|
|
340
|
+
<McpConfigDialog
|
|
341
|
+
open={mcpDialogOpen}
|
|
342
|
+
onClose={() => setMcpDialogOpen(false)}
|
|
343
|
+
/>
|
|
344
|
+
</Page>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DevAiHubPage } from './DevAiHubPage';
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import Box from '@mui/material/Box';
|
|
3
|
+
import Button from '@mui/material/Button';
|
|
4
|
+
import Chip from '@mui/material/Chip';
|
|
5
|
+
import Dialog from '@mui/material/Dialog';
|
|
6
|
+
import DialogActions from '@mui/material/DialogActions';
|
|
7
|
+
import DialogContent from '@mui/material/DialogContent';
|
|
8
|
+
import DialogTitle from '@mui/material/DialogTitle';
|
|
9
|
+
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
10
|
+
import IconButton from '@mui/material/IconButton';
|
|
11
|
+
import Switch from '@mui/material/Switch';
|
|
12
|
+
import Tab from '@mui/material/Tab';
|
|
13
|
+
import Tabs from '@mui/material/Tabs';
|
|
14
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
15
|
+
import Typography from '@mui/material/Typography';
|
|
16
|
+
import { useTheme } from '@mui/material/styles';
|
|
17
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
18
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
19
|
+
import StorageIcon from '@mui/icons-material/Storage';
|
|
20
|
+
import { useApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
|
21
|
+
import { ToolIcon } from '../ToolIcon';
|
|
22
|
+
import { useCopyToClipboard, useProviders } from '../../hooks';
|
|
23
|
+
import type { AiTool } from '@nospt/plugin-dev-ai-hub-common';
|
|
24
|
+
|
|
25
|
+
interface ToolConfig {
|
|
26
|
+
tool: AiTool;
|
|
27
|
+
label: string;
|
|
28
|
+
file: string;
|
|
29
|
+
description: string;
|
|
30
|
+
buildConfig: (mcpUrl: string) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TOOL_CONFIGS: ToolConfig[] = [
|
|
34
|
+
{
|
|
35
|
+
tool: 'claude-code',
|
|
36
|
+
label: 'Claude Code',
|
|
37
|
+
file: '.mcp.json',
|
|
38
|
+
description: 'Add to .mcp.json in your project root (or run via claude mcp add):',
|
|
39
|
+
buildConfig: url => JSON.stringify({
|
|
40
|
+
mcpServers: { 'dev-ai-hub': { type: 'http', url } },
|
|
41
|
+
}, null, 2),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
tool: 'github-copilot',
|
|
45
|
+
label: 'GitHub Copilot',
|
|
46
|
+
file: '.vscode/settings.json',
|
|
47
|
+
description: 'Add to your VS Code settings (.vscode/settings.json or user settings):',
|
|
48
|
+
buildConfig: url => JSON.stringify({
|
|
49
|
+
'github.copilot.chat.mcp.servers': { 'dev-ai-hub': { type: 'http', url } },
|
|
50
|
+
}, null, 2),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tool: 'google-gemini',
|
|
54
|
+
label: 'Google Gemini',
|
|
55
|
+
file: 'gemini-config.json',
|
|
56
|
+
description: 'Add to your Gemini CLI configuration:',
|
|
57
|
+
buildConfig: url => JSON.stringify({
|
|
58
|
+
mcpServers: { 'dev-ai-hub': { url } },
|
|
59
|
+
}, null, 2),
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function providerLabel(target: string): string {
|
|
64
|
+
return target.split('/').pop()?.replace(/\.git$/, '') ?? target;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface McpConfigDialogProps {
|
|
68
|
+
open: boolean;
|
|
69
|
+
onClose: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function McpConfigDialog({ open, onClose }: McpConfigDialogProps) {
|
|
73
|
+
const theme = useTheme();
|
|
74
|
+
const discoveryApi = useApi(discoveryApiRef);
|
|
75
|
+
const { copy: copyUrl, copied: copiedUrl } = useCopyToClipboard();
|
|
76
|
+
const { copy: copySnippet, copied: copiedSnippet } = useCopyToClipboard();
|
|
77
|
+
const [tab, setTab] = useState(0);
|
|
78
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
79
|
+
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
|
80
|
+
const [proactiveEnabled, setProactiveEnabled] = useState(false);
|
|
81
|
+
|
|
82
|
+
const { providers } = useProviders();
|
|
83
|
+
const showProviderFilter = providers.length > 1;
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (open) {
|
|
87
|
+
discoveryApi.getBaseUrl('dev-ai-hub').then(url => setBaseUrl(url));
|
|
88
|
+
}
|
|
89
|
+
}, [open, discoveryApi]);
|
|
90
|
+
|
|
91
|
+
const cfg = TOOL_CONFIGS[tab];
|
|
92
|
+
|
|
93
|
+
const buildMcpUrl = () => {
|
|
94
|
+
if (!baseUrl) return 'loading...';
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
params.set('tool', cfg.tool);
|
|
97
|
+
if (selectedProvider) params.set('provider', selectedProvider);
|
|
98
|
+
if (proactiveEnabled) params.set('proactive', 'true');
|
|
99
|
+
return `${baseUrl}/mcp?${params.toString()}`;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const mcpUrl = buildMcpUrl();
|
|
103
|
+
const configSnippet = baseUrl ? cfg.buildConfig(mcpUrl) : '';
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
107
|
+
<DialogTitle sx={{ pb: 0 }}>
|
|
108
|
+
<Typography variant="h6" fontWeight={700}>Configure MCP Server</Typography>
|
|
109
|
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
|
110
|
+
Connect your AI tool to the Dev AI Hub via Model Context Protocol.
|
|
111
|
+
</Typography>
|
|
112
|
+
</DialogTitle>
|
|
113
|
+
|
|
114
|
+
<DialogContent sx={{ pt: 1 }}>
|
|
115
|
+
<Tabs
|
|
116
|
+
value={tab}
|
|
117
|
+
onChange={(_, v) => setTab(v)}
|
|
118
|
+
sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
|
|
119
|
+
>
|
|
120
|
+
{TOOL_CONFIGS.map((t, i) => (
|
|
121
|
+
<Tab
|
|
122
|
+
key={t.tool}
|
|
123
|
+
value={i}
|
|
124
|
+
label={
|
|
125
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
126
|
+
<ToolIcon
|
|
127
|
+
tool={t.tool}
|
|
128
|
+
sx={{ fontSize: '1rem', color: theme.palette.mode === 'dark' ? '#fff' : undefined }}
|
|
129
|
+
/>
|
|
130
|
+
<span>{t.label}</span>
|
|
131
|
+
</Box>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
</Tabs>
|
|
136
|
+
|
|
137
|
+
{/* Provider filter — only shown when there are 2+ providers */}
|
|
138
|
+
{showProviderFilter && (
|
|
139
|
+
<Box sx={{ mb: 2 }}>
|
|
140
|
+
<Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 0.75 }}>
|
|
141
|
+
Scope to Provider
|
|
142
|
+
</Typography>
|
|
143
|
+
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
|
|
144
|
+
<Chip
|
|
145
|
+
label="All providers"
|
|
146
|
+
size="small"
|
|
147
|
+
clickable
|
|
148
|
+
onClick={() => setSelectedProvider('')}
|
|
149
|
+
sx={{
|
|
150
|
+
fontWeight: 600,
|
|
151
|
+
fontSize: '0.75rem',
|
|
152
|
+
borderRadius: 2,
|
|
153
|
+
border: '1.5px solid',
|
|
154
|
+
borderColor: !selectedProvider ? 'text.primary' : 'divider',
|
|
155
|
+
backgroundColor: !selectedProvider ? 'text.primary' : 'transparent',
|
|
156
|
+
color: !selectedProvider ? 'background.paper' : 'text.secondary',
|
|
157
|
+
transition: 'all 0.15s ease',
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
{providers.map(p => {
|
|
161
|
+
const isSelected = selectedProvider === p.id;
|
|
162
|
+
const label = providerLabel(p.target);
|
|
163
|
+
return (
|
|
164
|
+
<Chip
|
|
165
|
+
key={p.id}
|
|
166
|
+
icon={<StorageIcon sx={{ fontSize: '0.8rem !important', color: isSelected ? 'background.paper' : 'inherit' }} />}
|
|
167
|
+
label={label}
|
|
168
|
+
size="small"
|
|
169
|
+
clickable
|
|
170
|
+
onClick={() => setSelectedProvider(isSelected ? '' : p.id)}
|
|
171
|
+
sx={{
|
|
172
|
+
fontWeight: 600,
|
|
173
|
+
fontSize: '0.75rem',
|
|
174
|
+
borderRadius: 2,
|
|
175
|
+
border: '1.5px solid',
|
|
176
|
+
borderColor: isSelected ? 'text.primary' : 'divider',
|
|
177
|
+
backgroundColor: isSelected ? 'text.primary' : 'transparent',
|
|
178
|
+
color: isSelected ? 'background.paper' : 'text.secondary',
|
|
179
|
+
transition: 'all 0.15s ease',
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</Box>
|
|
185
|
+
</Box>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Proactive suggestions toggle */}
|
|
189
|
+
<Box sx={{ mb: 2 }}>
|
|
190
|
+
<FormControlLabel
|
|
191
|
+
control={
|
|
192
|
+
<Switch
|
|
193
|
+
size="small"
|
|
194
|
+
checked={proactiveEnabled}
|
|
195
|
+
onChange={e => setProactiveEnabled(e.target.checked)}
|
|
196
|
+
/>
|
|
197
|
+
}
|
|
198
|
+
label={
|
|
199
|
+
<Box>
|
|
200
|
+
<Typography variant="body2" fontWeight={600}>Proactive suggestions</Typography>
|
|
201
|
+
<Typography variant="caption" color="text.secondary">
|
|
202
|
+
The AI will automatically suggest relevant assets based on your project context.
|
|
203
|
+
Disable if you prefer to search manually.
|
|
204
|
+
</Typography>
|
|
205
|
+
</Box>
|
|
206
|
+
}
|
|
207
|
+
sx={{ alignItems: 'flex-start', ml: 0, gap: 1 }}
|
|
208
|
+
/>
|
|
209
|
+
</Box>
|
|
210
|
+
|
|
211
|
+
{/* MCP URL */}
|
|
212
|
+
<Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
|
213
|
+
MCP Endpoint
|
|
214
|
+
</Typography>
|
|
215
|
+
<Box
|
|
216
|
+
sx={{
|
|
217
|
+
display: 'flex',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
gap: 1,
|
|
220
|
+
mt: 0.5,
|
|
221
|
+
mb: 2,
|
|
222
|
+
px: 1.5,
|
|
223
|
+
py: 1,
|
|
224
|
+
borderRadius: 1.5,
|
|
225
|
+
border: '1px solid',
|
|
226
|
+
borderColor: 'divider',
|
|
227
|
+
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<Typography
|
|
231
|
+
variant="body2"
|
|
232
|
+
sx={{ flex: 1, fontFamily: 'monospace', fontSize: '0.8rem', wordBreak: 'break-all' }}
|
|
233
|
+
>
|
|
234
|
+
{mcpUrl}
|
|
235
|
+
</Typography>
|
|
236
|
+
<Tooltip title={copiedUrl ? 'Copied!' : 'Copy URL'}>
|
|
237
|
+
<IconButton size="small" onClick={() => copyUrl(mcpUrl)}>
|
|
238
|
+
{copiedUrl ? <CheckIcon fontSize="small" color="success" /> : <ContentCopyIcon fontSize="small" />}
|
|
239
|
+
</IconButton>
|
|
240
|
+
</Tooltip>
|
|
241
|
+
</Box>
|
|
242
|
+
|
|
243
|
+
{/* Config snippet */}
|
|
244
|
+
<Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
|
245
|
+
{cfg.file}
|
|
246
|
+
</Typography>
|
|
247
|
+
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1, mt: 0.5 }}>
|
|
248
|
+
{cfg.description}
|
|
249
|
+
</Typography>
|
|
250
|
+
<Box sx={{ position: 'relative' }}>
|
|
251
|
+
<Box
|
|
252
|
+
component="pre"
|
|
253
|
+
sx={{
|
|
254
|
+
m: 0,
|
|
255
|
+
p: 2,
|
|
256
|
+
borderRadius: 2,
|
|
257
|
+
border: '1px solid',
|
|
258
|
+
borderColor: 'divider',
|
|
259
|
+
backgroundColor: theme.palette.mode === 'dark' ? '#0d1117' : '#f6f8fa',
|
|
260
|
+
color: theme.palette.mode === 'dark' ? '#e6edf3' : '#24292f',
|
|
261
|
+
fontFamily: 'monospace',
|
|
262
|
+
fontSize: '0.8rem',
|
|
263
|
+
overflowX: 'auto',
|
|
264
|
+
whiteSpace: 'pre',
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
{configSnippet}
|
|
268
|
+
</Box>
|
|
269
|
+
<Tooltip title={copiedSnippet ? 'Copied!' : 'Copy config'}>
|
|
270
|
+
<IconButton
|
|
271
|
+
size="small"
|
|
272
|
+
onClick={() => copySnippet(configSnippet)}
|
|
273
|
+
sx={{
|
|
274
|
+
position: 'absolute',
|
|
275
|
+
top: 8,
|
|
276
|
+
right: 8,
|
|
277
|
+
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)',
|
|
278
|
+
'&:hover': {
|
|
279
|
+
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.12)',
|
|
280
|
+
},
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{copiedSnippet ? <CheckIcon fontSize="small" color="success" /> : <ContentCopyIcon fontSize="small" />}
|
|
284
|
+
</IconButton>
|
|
285
|
+
</Tooltip>
|
|
286
|
+
</Box>
|
|
287
|
+
|
|
288
|
+
<Typography variant="caption" color="text.disabled" sx={{ display: 'block', mt: 1.5 }}>
|
|
289
|
+
💡 Omit <code>?tool=</code> from the URL to receive assets for all AI tools.
|
|
290
|
+
{showProviderFilter && ' Omit ?provider= to receive assets from all repositories.'}
|
|
291
|
+
{' '}Proactive suggestions add <code>?proactive=true</code> and register the{' '}
|
|
292
|
+
<code>suggest_assets</code> tool and <code>check_for_assets</code> prompt.
|
|
293
|
+
</Typography>
|
|
294
|
+
</DialogContent>
|
|
295
|
+
|
|
296
|
+
<DialogActions>
|
|
297
|
+
<Button onClick={onClose}>Close</Button>
|
|
298
|
+
</DialogActions>
|
|
299
|
+
</Dialog>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { McpConfigDialog } from './McpConfigDialog';
|