@nospt/plugin-dev-ai-hub 0.1.0 → 0.2.1

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.
Files changed (71) hide show
  1. package/dev/index.tsx +17 -217
  2. package/dist/api/DevAiHubClient.esm.js +2 -0
  3. package/dist/api/DevAiHubClient.esm.js.map +1 -1
  4. package/dist/components/AssetCard/AssetCard.esm.js +60 -134
  5. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -1
  6. package/dist/components/AssetCard/AssetCard.module.css.esm.js +8 -0
  7. package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +1 -0
  8. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +175 -281
  9. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -1
  10. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +8 -0
  11. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +1 -0
  12. package/dist/components/AssetFilters/AssetFilters.esm.js +94 -134
  13. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -1
  14. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +8 -0
  15. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +1 -0
  16. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +70 -126
  17. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -1
  18. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +8 -0
  19. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +1 -0
  20. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +64 -189
  21. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -1
  22. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +8 -0
  23. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +1 -0
  24. package/dist/components/McpConfigPage/McpConfigPage.esm.js +200 -0
  25. package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +1 -0
  26. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +8 -0
  27. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +1 -0
  28. package/dist/components/McpConfigPage/index.esm.js +6 -0
  29. package/dist/components/McpConfigPage/index.esm.js.map +1 -0
  30. package/dist/components/ToolIcon/ToolIcon.esm.js +25 -13
  31. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  32. package/dist/index.d.ts +10 -5
  33. package/dist/index.esm.js +1 -1
  34. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  35. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  36. package/dist/plugin.esm.js +24 -6
  37. package/dist/plugin.esm.js.map +1 -1
  38. package/dist/routes.esm.js +2 -1
  39. package/dist/routes.esm.js.map +1 -1
  40. package/package.json +24 -14
  41. package/package.json-prepack +24 -14
  42. package/src/components/AssetCard/AssetCard.module.css +153 -0
  43. package/src/components/AssetCard/AssetCard.tsx +79 -148
  44. package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +207 -0
  45. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +228 -292
  46. package/src/components/AssetFilters/AssetFilters.module.css +67 -0
  47. package/src/components/AssetFilters/AssetFilters.tsx +136 -178
  48. package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +72 -0
  49. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +95 -135
  50. package/src/components/DevAiHubPage/DevAiHubPage.module.css +105 -0
  51. package/src/components/DevAiHubPage/DevAiHubPage.tsx +73 -222
  52. package/src/components/McpConfigDialog/McpConfigDialog.module.css +111 -0
  53. package/src/components/McpConfigDialog/McpConfigDialog.tsx +178 -208
  54. package/src/components/McpConfigPage/McpConfigPage.module.css +121 -0
  55. package/src/components/McpConfigPage/McpConfigPage.tsx +242 -0
  56. package/src/components/McpConfigPage/index.ts +1 -0
  57. package/src/components/ToolIcon/ToolIcon.tsx +31 -15
  58. package/src/css-modules.d.ts +4 -0
  59. package/src/index.ts +1 -1
  60. package/src/plugin.tsx +23 -3
  61. package/src/routes.ts +1 -0
  62. package/tests/e2e/.auth.json +14 -0
  63. package/tests/e2e/asset-detail.spec.ts +90 -0
  64. package/tests/e2e/asset-filtering.spec.ts +145 -0
  65. package/tests/e2e/asset-installation.spec.ts +74 -0
  66. package/tests/e2e/dev-ai-hub-page.spec.ts +89 -0
  67. package/tests/e2e/fixtures/base.ts +61 -0
  68. package/tests/e2e/fixtures/mock-api.ts +221 -0
  69. package/tests/e2e/mcp-config.spec.ts +88 -0
  70. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +0 -274
  71. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +0 -1
@@ -1,25 +1,16 @@
1
1
  import type { ElementType } from 'react';
2
- import Box from '@mui/material/Box';
3
- import Chip from '@mui/material/Chip';
4
- import InputAdornment from '@mui/material/InputAdornment';
5
- import TextField from '@mui/material/TextField';
6
- import Typography from '@mui/material/Typography';
7
- import SearchIcon from '@mui/icons-material/Search';
8
- import StorageIcon from '@mui/icons-material/Storage';
9
- import ArticleIcon from '@mui/icons-material/Article';
10
- import SmartToyIcon from '@mui/icons-material/SmartToy';
11
- import BuildIcon from '@mui/icons-material/Build';
12
- import AccountTreeIcon from '@mui/icons-material/AccountTree';
13
- import AppsIcon from '@mui/icons-material/Apps';
2
+ import { Box, Flex, Text, SearchField, ToggleButton, ToggleButtonGroup } from '@backstage/ui';
3
+ import { RiAppsLine, RiArticleLine, RiRobot2Line, RiToolsLine, RiGitBranchLine, RiDatabase2Line } from '@remixicon/react';
14
4
  import type { AssetType, AiTool, AiHubProvider } from '@nospt/plugin-dev-ai-hub-common';
15
5
  import { ToolIcon } from '../ToolIcon';
6
+ import styles from './AssetFilters.module.css';
16
7
 
17
8
  const ASSET_TYPES: { value: AssetType | 'all'; label: string; color: string; Icon: ElementType }[] = [
18
- { value: 'all', label: 'All', color: '#64748b', Icon: AppsIcon },
19
- { value: 'instruction', label: 'Instructions', color: '#2563EB', Icon: ArticleIcon },
20
- { value: 'agent', label: 'Agents', color: '#7C3AED', Icon: SmartToyIcon },
21
- { value: 'skill', label: 'Skills', color: '#059669', Icon: BuildIcon },
22
- { value: 'workflow', label: 'Workflows', color: '#D97706', Icon: AccountTreeIcon },
9
+ { value: 'all', label: 'All', color: '#DCDDE1', Icon: RiAppsLine },
10
+ { value: 'instruction', label: 'Instructions', color: '#54A0FF', Icon: RiArticleLine },
11
+ { value: 'agent', label: 'Agents', color: '#FF6B9D', Icon: RiRobot2Line },
12
+ { value: 'skill', label: 'Skills', color: '#6AB04C', Icon: RiToolsLine },
13
+ { value: 'workflow', label: 'Workflows', color: '#F9CA24', Icon: RiGitBranchLine },
23
14
  ];
24
15
 
25
16
  const AI_TOOLS: { value: AiTool | 'all'; label: string }[] = [
@@ -70,192 +61,159 @@ export function AssetFilters({ value, onChange, availableTags = [], providers }:
70
61
  };
71
62
 
72
63
  return (
73
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
74
- <TextField
75
- fullWidth
76
- size="small"
64
+ <Flex className={styles.container}>
65
+ <SearchField
66
+ aria-label="Search assets"
67
+ className={styles.searchField}
77
68
  placeholder="Search assets by name, description or content…"
78
69
  value={value.search}
79
- onChange={e => onChange({ ...value, search: e.target.value })}
80
- InputProps={{
81
- startAdornment: (
82
- <InputAdornment position="start">
83
- <SearchIcon fontSize="small" />
84
- </InputAdornment>
85
- ),
86
- }}
87
- sx={{
88
- '& .MuiOutlinedInput-root': {
89
- borderRadius: 2,
90
- },
91
- }}
70
+ onChange={v => onChange({ ...value, search: v })}
92
71
  />
93
72
 
94
- <Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap', alignItems: 'flex-start' }}>
73
+ <Flex className={styles.filtersRow}>
95
74
  {/* Type filter */}
96
75
  <Box>
97
- <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }}>
76
+ <Text variant="body-x-small" color="secondary" className={styles.filterLabel}>
98
77
  Type
99
- </Typography>
100
- <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
101
- {ASSET_TYPES.map(t => {
102
- const isSelected = selectedType === t.value;
103
- const TypeIcon = t.Icon;
104
- return (
105
- <Chip
106
- key={t.value}
107
- icon={<TypeIcon sx={{ fontSize: '0.9rem !important', color: isSelected ? '#fff !important' : t.color }} />}
108
- label={t.label}
109
- size="small"
110
- clickable
111
- onClick={() => handleTypeClick(t.value as AssetType | 'all')}
112
- sx={{
113
- fontWeight: 600,
114
- fontSize: '0.75rem',
115
- borderRadius: 2,
116
- border: '1.5px solid',
117
- borderColor: isSelected ? t.color : `${t.color}60`,
118
- backgroundColor: isSelected ? t.color : 'transparent',
119
- color: isSelected ? '#fff' : t.color,
120
- transition: 'all 0.15s ease',
121
- '&:hover': {
122
- backgroundColor: isSelected ? t.color : `${t.color}18`,
123
- borderColor: t.color,
124
- },
125
- }}
126
- />
127
- );
128
- })}
129
- </Box>
78
+ </Text>
79
+ <Flex className={styles.filterChips}>
80
+ {ASSET_TYPES.map(t => {
81
+ const isSelected = selectedType === t.value;
82
+ const TypeIcon = t.Icon;
83
+ return (
84
+ <ToggleButton
85
+ key={t.value}
86
+ id={t.value}
87
+ size="small"
88
+ className={`${styles.filterChip} ${isSelected ? styles.filterChipSelected : ''}`}
89
+ iconStart={<TypeIcon size={14} style={{ color: isSelected ? '#fff' : t.color }} />}
90
+ isSelected={isSelected}
91
+ onChange={() => handleTypeClick(t.value as AssetType | 'all')}
92
+ style={{
93
+ borderColor: isSelected ? t.color : `${t.color}30`,
94
+ backgroundColor: isSelected ? t.color : `${t.color}10`,
95
+ color: isSelected ? '#fff' : t.color,
96
+ }}
97
+ >
98
+ {t.label}
99
+ </ToggleButton>
100
+ );
101
+ })}
102
+ </Flex>
130
103
  </Box>
131
104
 
132
105
  {/* AI Tool filter */}
133
106
  <Box>
134
- <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }}>
107
+ <Text variant="body-x-small" color="secondary" className={styles.filterLabel}>
135
108
  AI Tool
136
- </Typography>
137
- <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
138
- {AI_TOOLS.map(t => {
139
- const isSelected = selectedTool === t.value;
140
- const iconEl = t.value !== 'all'
141
- ? <ToolIcon tool={t.value as AiTool} branded={!isSelected} sx={{ fontSize: '0.85rem !important', color: isSelected ? 'background.paper' : 'inherit' }} />
142
- : undefined;
143
- return (
144
- <Chip
145
- key={t.value}
146
- icon={iconEl}
147
- label={t.label}
148
- size="small"
149
- clickable
150
- onClick={() => handleToolClick(t.value as AiTool | 'all')}
151
- sx={{
152
- fontWeight: 600,
153
- fontSize: '0.75rem',
154
- borderRadius: 2,
155
- border: '1.5px solid',
156
- borderColor: isSelected ? 'text.primary' : 'divider',
157
- backgroundColor: isSelected ? 'text.primary' : 'transparent',
158
- color: isSelected ? 'background.paper' : 'text.secondary',
159
- transition: 'all 0.15s ease',
160
- '&:hover': {
161
- borderColor: 'text.primary',
162
- backgroundColor: isSelected ? 'text.primary' : 'action.hover',
163
- },
164
- }}
165
- />
166
- );
167
- })}
168
- </Box>
109
+ </Text>
110
+ <Flex className={styles.filterChips}>
111
+ {AI_TOOLS.map(t => {
112
+ const isSelected = selectedTool === t.value;
113
+ const iconEl = t.value !== 'all'
114
+ ? <ToolIcon tool={t.value as AiTool} branded size={14} />
115
+ : undefined;
116
+ return (
117
+ <ToggleButton
118
+ key={t.value}
119
+ id={t.value}
120
+ size="small"
121
+ className={`${styles.filterChip} ${isSelected ? styles.toolChipSelected : ''}`}
122
+ iconStart={iconEl}
123
+ isSelected={isSelected}
124
+ onChange={() => handleToolClick(t.value as AiTool | 'all')}
125
+ style={{
126
+ borderColor: isSelected ? 'var(--bui-fg-link)' : 'var(--bui-border-1)',
127
+ backgroundColor: isSelected ? 'var(--bui-bg-accent-1)' : 'transparent',
128
+ color: isSelected ? 'var(--bui-fg-link)' : 'var(--bui-fg-secondary)',
129
+ }}
130
+ >
131
+ {t.label}
132
+ </ToggleButton>
133
+ );
134
+ })}
135
+ </Flex>
169
136
  </Box>
170
137
 
171
138
  {/* Provider filter — only shown when there are 2+ providers */}
172
139
  {showProviderFilter && (
173
140
  <Box>
174
- <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }}>
141
+ <Text variant="body-x-small" color="secondary" className={styles.filterLabel}>
175
142
  Provider
176
- </Typography>
177
- <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
178
- <Chip
179
- icon={<AppsIcon sx={{ fontSize: '0.9rem !important' }} />}
180
- label="All"
181
- size="small"
182
- clickable
183
- onClick={() => handleProviderClick(undefined)}
184
- sx={{
185
- fontWeight: 600,
186
- fontSize: '0.75rem',
187
- borderRadius: 2,
188
- border: '1.5px solid',
189
- borderColor: !value.providerId ? 'text.primary' : 'divider',
190
- backgroundColor: !value.providerId ? 'text.primary' : 'transparent',
191
- color: !value.providerId ? 'background.paper' : 'text.secondary',
192
- transition: 'all 0.15s ease',
193
- '&:hover': {
194
- borderColor: 'text.primary',
195
- backgroundColor: !value.providerId ? 'text.primary' : 'action.hover',
196
- },
197
- }}
198
- />
199
- {providers.map(p => {
200
- const isSelected = value.providerId === p.id;
201
- const label = p.target.split('/').slice(-1)[0]?.replace(/\.git$/, '') ?? p.id;
202
- return (
203
- <Chip
204
- key={p.id}
205
- icon={<StorageIcon sx={{ fontSize: '0.85rem !important', color: isSelected ? 'background.paper' : 'inherit' }} />}
206
- label={label}
207
- size="small"
208
- clickable
209
- onClick={() => handleProviderClick(p.id)}
210
- sx={{
211
- fontWeight: 600,
212
- fontSize: '0.75rem',
213
- borderRadius: 2,
214
- border: '1.5px solid',
215
- borderColor: isSelected ? 'text.primary' : 'divider',
216
- backgroundColor: isSelected ? 'text.primary' : 'transparent',
217
- color: isSelected ? 'background.paper' : 'text.secondary',
218
- transition: 'all 0.15s ease',
219
- '&:hover': {
220
- borderColor: 'text.primary',
221
- backgroundColor: isSelected ? 'text.primary' : 'action.hover',
222
- },
223
- }}
224
- />
225
- );
226
- })}
227
- </Box>
143
+ </Text>
144
+ <Flex className={styles.filterChips}>
145
+ <ToggleButton
146
+ id="all-providers"
147
+ size="small"
148
+ className={`${styles.filterChip} ${!value.providerId ? styles.filterChipSelected : ''}`}
149
+ iconStart={<RiAppsLine size={14} />}
150
+ isSelected={!value.providerId}
151
+ onChange={() => handleProviderClick(undefined)}
152
+ style={{
153
+ borderColor: !value.providerId ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
154
+ backgroundColor: !value.providerId ? 'var(--bui-fg-primary)' : 'transparent',
155
+ color: !value.providerId ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
156
+ }}
157
+ >
158
+ All
159
+ </ToggleButton>
160
+ {providers.map(p => {
161
+ const isSelected = value.providerId === p.id;
162
+ const label = p.target.split('/').slice(-1)[0]?.replace(/\.git$/, '') ?? p.id;
163
+ return (
164
+ <ToggleButton
165
+ key={p.id}
166
+ id={p.id}
167
+ size="small"
168
+ className={`${styles.filterChip} ${isSelected ? styles.filterChipSelected : ''}`}
169
+ iconStart={<RiDatabase2Line size={14} style={{ color: isSelected ? 'var(--bui-bg-neutral-1)' : 'inherit' }} />}
170
+ isSelected={isSelected}
171
+ onChange={() => handleProviderClick(isSelected ? undefined : p.id)}
172
+ style={{
173
+ borderColor: isSelected ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
174
+ backgroundColor: isSelected ? 'var(--bui-fg-primary)' : 'transparent',
175
+ color: isSelected ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
176
+ }}
177
+ >
178
+ {label}
179
+ </ToggleButton>
180
+ );
181
+ })}
182
+ </Flex>
228
183
  </Box>
229
184
  )}
230
- </Box>
185
+ </Flex>
231
186
 
232
187
  {availableTags.length > 0 && (
233
188
  <Box>
234
- <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }}>
189
+ <Text variant="body-x-small" color="secondary" className={styles.filterLabel}>
235
190
  Tags
236
- </Typography>
237
- <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
238
- {availableTags.map(tag => (
239
- <Chip
240
- key={tag}
241
- label={`#${tag}`}
242
- size="small"
243
- clickable
244
- onClick={() => handleTagToggle(tag)}
245
- sx={{
246
- fontSize: '0.7rem',
247
- fontWeight: value.tags.includes(tag) ? 700 : 400,
248
- borderRadius: 1.5,
249
- backgroundColor: value.tags.includes(tag) ? 'primary.main' : 'transparent',
250
- color: value.tags.includes(tag) ? '#fff' : 'text.secondary',
251
- border: '1px solid',
252
- borderColor: value.tags.includes(tag) ? 'primary.main' : 'divider',
253
- }}
254
- />
255
- ))}
256
- </Box>
191
+ </Text>
192
+ <Flex className={styles.filterChips}>
193
+ {availableTags.map(tag => {
194
+ const isActive = value.tags.includes(tag);
195
+ return (
196
+ <ToggleButton
197
+ key={tag}
198
+ id={tag}
199
+ size="small"
200
+ className={`${styles.tagChip} ${isActive ? styles.tagChipSelected : ''}`}
201
+ isSelected={isActive}
202
+ onChange={() => handleTagToggle(tag)}
203
+ style={{
204
+ fontWeight: isActive ? 700 : 400,
205
+ backgroundColor: isActive ? 'var(--bui-bg-solid)' : 'transparent',
206
+ color: isActive ? '#fff' : 'var(--bui-fg-secondary)',
207
+ borderColor: isActive ? 'var(--bui-bg-solid)' : 'var(--bui-border-1)',
208
+ }}
209
+ >
210
+ #{tag}
211
+ </ToggleButton>
212
+ );
213
+ })}
214
+ </Flex>
257
215
  </Box>
258
216
  )}
259
- </Box>
217
+ </Flex>
260
218
  );
261
219
  }
@@ -0,0 +1,72 @@
1
+ @layer components {
2
+ .subtitle {
3
+ margin-top: var(--bui-space-1);
4
+ font-weight: 400;
5
+ }
6
+
7
+ .loadingContainer {
8
+ display: flex;
9
+ justify-content: center;
10
+ padding: var(--bui-space-6) 0;
11
+ }
12
+
13
+ .zipInfo {
14
+ border: 1px solid #00D2D3;
15
+ border-radius: var(--bui-radius-2);
16
+ padding: var(--bui-space-3);
17
+ background-color: color-mix(in srgb, #00D2D3 7%, transparent);
18
+ }
19
+
20
+ .zipHeader {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--bui-space-2);
24
+ margin-bottom: var(--bui-space-2);
25
+ }
26
+
27
+ .zipFiles {
28
+ display: flex;
29
+ gap: var(--bui-space-1);
30
+ flex-wrap: wrap;
31
+ }
32
+
33
+ .monoTag {
34
+ font-family: monospace;
35
+ font-size: 0.7rem;
36
+ height: 20px;
37
+ }
38
+
39
+ .toolSection {
40
+ border: 1px solid var(--bui-border-1);
41
+ border-radius: var(--bui-radius-2);
42
+ padding: var(--bui-space-3);
43
+ }
44
+
45
+ .toolHeader {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: var(--bui-space-2);
49
+ margin-bottom: var(--bui-space-2);
50
+ }
51
+
52
+ .installPathBox {
53
+ background-color: var(--bui-bg-neutral-1-hover);
54
+ border: 1px solid var(--bui-border-1);
55
+ border-radius: var(--bui-radius-2);
56
+ padding: var(--bui-space-2) var(--bui-space-3);
57
+ font-family: monospace;
58
+ font-size: 0.78rem;
59
+ color: var(--bui-fg-primary);
60
+ word-break: break-all;
61
+ margin-bottom: 10px;
62
+ }
63
+
64
+ .toolActions {
65
+ display: flex;
66
+ gap: var(--bui-space-2);
67
+ }
68
+
69
+ .toolActions > * {
70
+ flex: 1;
71
+ }
72
+ }