@julianpedro/plugin-dev-ai-hub 0.2.0 → 0.4.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.
Files changed (50) hide show
  1. package/dist/api/DevAiHubClient.esm.js +15 -2
  2. package/dist/api/DevAiHubClient.esm.js.map +1 -1
  3. package/dist/components/AdminPage/AdminPage.esm.js +106 -0
  4. package/dist/components/AdminPage/AdminPage.esm.js.map +1 -0
  5. package/dist/components/AdminPage/index.esm.js +6 -0
  6. package/dist/components/AdminPage/index.esm.js.map +1 -0
  7. package/dist/components/AssetCard/AssetCard.esm.js +117 -26
  8. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -1
  9. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +271 -87
  10. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -1
  11. package/dist/components/AssetFilters/AssetFilters.esm.js +105 -44
  12. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -1
  13. package/dist/components/AssetHelpDialog/AssetHelpDialog.esm.js +87 -0
  14. package/dist/components/AssetHelpDialog/AssetHelpDialog.esm.js.map +1 -0
  15. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +328 -108
  16. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -1
  17. package/dist/components/AssetsTab/AssetsTab.esm.js +221 -0
  18. package/dist/components/AssetsTab/AssetsTab.esm.js.map +1 -0
  19. package/dist/components/AssetsTab/index.esm.js +6 -0
  20. package/dist/components/AssetsTab/index.esm.js.map +1 -0
  21. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +266 -134
  22. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -1
  23. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +20 -297
  24. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +1 -1
  25. package/dist/components/McpPage/McpPage.esm.js +478 -0
  26. package/dist/components/McpPage/McpPage.esm.js.map +1 -0
  27. package/dist/components/McpPage/index.esm.js +6 -0
  28. package/dist/components/McpPage/index.esm.js.map +1 -0
  29. package/dist/components/ModelIcon/ModelBadge.esm.js +73 -0
  30. package/dist/components/ModelIcon/ModelBadge.esm.js.map +1 -0
  31. package/dist/components/ModelIcon/ModelIcon.esm.js +45 -0
  32. package/dist/components/ModelIcon/ModelIcon.esm.js.map +1 -0
  33. package/dist/components/ToolIcon/ToolIcon.esm.js +4 -1
  34. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  35. package/dist/context/UiConfigContext.esm.js +79 -0
  36. package/dist/context/UiConfigContext.esm.js.map +1 -0
  37. package/dist/hooks/index.esm.js +36 -1
  38. package/dist/hooks/index.esm.js.map +1 -1
  39. package/dist/index.d.ts +146 -23
  40. package/dist/index.esm.js +1 -0
  41. package/dist/index.esm.js.map +1 -1
  42. package/dist/locales/es.esm.js +121 -0
  43. package/dist/locales/es.esm.js.map +1 -0
  44. package/dist/locales/pt-BR.esm.js +121 -0
  45. package/dist/locales/pt-BR.esm.js.map +1 -0
  46. package/dist/plugin.esm.js +35 -6
  47. package/dist/plugin.esm.js.map +1 -1
  48. package/dist/translation.esm.js +151 -0
  49. package/dist/translation.esm.js.map +1 -0
  50. package/package.json +15 -5
@@ -1,28 +1,47 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useState, useMemo } from 'react';
3
3
  import { useSearchParams } from 'react-router-dom';
4
+ import { darken } from '@mui/material/styles';
4
5
  import Box from '@mui/material/Box';
5
6
  import Button from '@mui/material/Button';
7
+ import CircularProgress from '@mui/material/CircularProgress';
8
+ import Divider from '@mui/material/Divider';
9
+ import Drawer from '@mui/material/Drawer';
6
10
  import Grid from '@mui/material/Grid';
11
+ import IconButton from '@mui/material/IconButton';
7
12
  import Pagination from '@mui/material/Pagination';
8
13
  import Skeleton from '@mui/material/Skeleton';
14
+ import Snackbar from '@mui/material/Snackbar';
9
15
  import Tooltip from '@mui/material/Tooltip';
10
16
  import Typography from '@mui/material/Typography';
11
- import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
12
17
  import ArticleIcon from '@mui/icons-material/Article';
13
18
  import SmartToyIcon from '@mui/icons-material/SmartToy';
14
19
  import BuildIcon from '@mui/icons-material/Build';
15
20
  import AccountTreeIcon from '@mui/icons-material/AccountTree';
21
+ import ChatIcon from '@mui/icons-material/Chat';
22
+ import Inventory2Icon from '@mui/icons-material/Inventory2';
23
+ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
24
+ import CloseIcon from '@mui/icons-material/Close';
25
+ import CloudSyncIcon from '@mui/icons-material/CloudSync';
26
+ import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
27
+ import ExtensionIcon from '@mui/icons-material/Extension';
16
28
  import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
17
29
  import HubIcon from '@mui/icons-material/Hub';
30
+ import SyncIcon from '@mui/icons-material/Sync';
18
31
  import { Page, Header, Content } from '@backstage/core-components';
32
+ import { useTranslationRef } from '@backstage/frontend-plugin-api';
33
+ import { usePermission } from '@backstage/plugin-permission-react';
34
+ import { devAiHubSyncPermission } from '@julianpedro/plugin-dev-ai-hub-common';
35
+ import { devAiHubTranslationRef } from '../../translation.esm.js';
19
36
  import { AssetCard } from '../AssetCard/AssetCard.esm.js';
20
37
  import { AssetFilters } from '../AssetFilters/AssetFilters.esm.js';
21
38
  import { AssetDetailPanel } from '../AssetDetailPanel/AssetDetailPanel.esm.js';
22
39
  import { AssetInstallDialog } from '../AssetInstallDialog/AssetInstallDialog.esm.js';
40
+ import { AssetHelpDialog } from '../AssetHelpDialog/AssetHelpDialog.esm.js';
23
41
  import { McpConfigDialog } from '../McpConfigDialog/McpConfigDialog.esm.js';
24
42
  import { ToolIcon } from '../ToolIcon/ToolIcon.esm.js';
25
- import { useStats, useProviders, useAssets } from '../../hooks/index.esm.js';
43
+ import { useSyncProvider, useStats, useProviders, useMcpCatalog, useAssets } from '../../hooks/index.esm.js';
44
+ import { useTypeConfig } from '../../context/UiConfigContext.esm.js';
26
45
 
27
46
  const SUPPORTED_TOOLS = ["claude-code", "github-copilot", "google-gemini", "cursor"];
28
47
  const TOOL_LABELS = {
@@ -32,13 +51,6 @@ const TOOL_LABELS = {
32
51
  "google-gemini": "Google Gemini",
33
52
  "cursor": "Cursor"
34
53
  };
35
- function timeAgo(iso) {
36
- const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1e3);
37
- if (diff < 60) return "just now";
38
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
39
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
40
- return `${Math.floor(diff / 86400)}d ago`;
41
- }
42
54
  const DEFAULT_FILTERS = {
43
55
  types: [],
44
56
  tools: [],
@@ -47,43 +59,34 @@ const DEFAULT_FILTERS = {
47
59
  providerId: void 0
48
60
  };
49
61
  const PAGE_SIZE = 24;
50
- const STATS_CONFIG = [
51
- {
52
- key: "instruction",
53
- label: "Instructions",
54
- Icon: ArticleIcon,
55
- gradient: "linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%)",
56
- shadow: "#2563EB40"
57
- },
58
- {
59
- key: "agent",
60
- label: "Agents",
61
- Icon: SmartToyIcon,
62
- gradient: "linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)",
63
- shadow: "#7C3AED40"
64
- },
65
- {
66
- key: "skill",
67
- label: "Skills",
68
- Icon: BuildIcon,
69
- gradient: "linear-gradient(135deg, #059669 0%, #047857 100%)",
70
- shadow: "#05966940"
71
- },
72
- {
73
- key: "workflow",
74
- label: "Workflows",
75
- Icon: AccountTreeIcon,
76
- gradient: "linear-gradient(135deg, #D97706 0%, #B45309 100%)",
77
- shadow: "#D9770640"
78
- }
79
- ];
62
+ const STATS_META = {
63
+ instruction: { label: "Instructions", Icon: ArticleIcon },
64
+ agent: { label: "Agents", Icon: SmartToyIcon },
65
+ skill: { label: "Skills", Icon: BuildIcon },
66
+ workflow: { label: "Workflows", Icon: AccountTreeIcon },
67
+ prompt: { label: "Prompts", Icon: ChatIcon },
68
+ bundle: { label: "Bundles", Icon: Inventory2Icon }
69
+ };
70
+ function timeAgo(iso, translate) {
71
+ const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1e3);
72
+ if (diff < 60) return translate("devAiHubPage.timeJustNow") ?? "";
73
+ if (diff < 3600) return translate("devAiHubPage.timeMinutesAgo", { count: Math.floor(diff / 60) }) ?? "";
74
+ if (diff < 86400) return translate("devAiHubPage.timeHoursAgo", { count: Math.floor(diff / 3600) }) ?? "";
75
+ return translate("devAiHubPage.timeDaysAgo", { count: Math.floor(diff / 86400) }) ?? "";
76
+ }
80
77
  function DevAiHubPage() {
78
+ const { t } = useTranslationRef(devAiHubTranslationRef);
81
79
  const [filters, setFilters] = useState(DEFAULT_FILTERS);
82
80
  const [page, setPage] = useState(1);
83
81
  const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
82
+ const [syncSnackbar, setSyncSnackbar] = useState(false);
83
+ const [providersDrawerOpen, setProvidersDrawerOpen] = useState(false);
84
+ const { allowed: canSync } = usePermission({ permission: devAiHubSyncPermission });
85
+ const { syncing, triggerSync, triggerSyncAll } = useSyncProvider();
84
86
  const [searchParams, setSearchParams] = useSearchParams();
85
87
  const selectedAssetId = searchParams.get("assetId");
86
88
  const installAssetId = searchParams.get("installId");
89
+ const helpAssetId = searchParams.get("helpId");
87
90
  const handleViewAsset = (id) => setSearchParams((p) => {
88
91
  const n = new URLSearchParams(p);
89
92
  n.set("assetId", id);
@@ -104,20 +107,29 @@ function DevAiHubPage() {
104
107
  n.delete("installId");
105
108
  return n;
106
109
  });
110
+ const handleHelpAsset = (id) => setSearchParams((p) => {
111
+ const n = new URLSearchParams(p);
112
+ n.set("helpId", id);
113
+ return n;
114
+ });
115
+ const handleCloseHelp = () => setSearchParams((p) => {
116
+ const n = new URLSearchParams(p);
117
+ n.delete("helpId");
118
+ return n;
119
+ });
120
+ const { typeColors, statsCards } = useTypeConfig();
107
121
  const { stats } = useStats();
108
122
  const { providers } = useProviders();
109
- const apiFilter = useMemo(
110
- () => ({
111
- type: filters.types.length === 1 ? filters.types[0] : void 0,
112
- tool: filters.tools.length === 1 ? filters.tools[0] : void 0,
113
- search: filters.search || void 0,
114
- tags: filters.tags.length > 0 ? filters.tags : void 0,
115
- providerId: filters.providerId || void 0,
116
- page,
117
- pageSize: PAGE_SIZE
118
- }),
119
- [filters, page]
120
- );
123
+ const { catalog } = useMcpCatalog();
124
+ const apiFilter = useMemo(() => ({
125
+ type: filters.types.length === 1 ? filters.types[0] : void 0,
126
+ tool: filters.tools.length === 1 ? filters.tools[0] : void 0,
127
+ search: filters.search || void 0,
128
+ tags: filters.tags.length > 0 ? filters.tags : void 0,
129
+ providerId: filters.providerId || void 0,
130
+ page,
131
+ pageSize: PAGE_SIZE
132
+ }), [filters, page]);
121
133
  const { result, loading } = useAssets(apiFilter);
122
134
  const handleFiltersChange = (next) => {
123
135
  setFilters(next);
@@ -127,7 +139,7 @@ function DevAiHubPage() {
127
139
  const availableTags = useMemo(() => {
128
140
  if (!result) return [];
129
141
  const tagSet = /* @__PURE__ */ new Set();
130
- result.items.forEach((a) => a.tags.forEach((t) => tagSet.add(t)));
142
+ result.items.forEach((a) => a.tags.forEach((tag) => tagSet.add(tag)));
131
143
  return Array.from(tagSet).sort();
132
144
  }, [result]);
133
145
  return /* @__PURE__ */ jsxs(Page, { themeId: "tool", children: [
@@ -156,7 +168,11 @@ function DevAiHubPage() {
156
168
  ),
157
169
  /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.3 }, children: [
158
170
  /* @__PURE__ */ jsx(Box, { sx: { fontSize: "1.75rem", fontWeight: 700, lineHeight: 1, color: "#fff" }, children: "Dev AI Hub" }),
159
- /* @__PURE__ */ jsx(Box, { sx: { fontSize: "0.85rem", fontWeight: 400, color: "rgba(255,255,255,0.75)", lineHeight: 1 }, children: stats ? `${stats.totalAssets} assets \xB7 ${Object.keys(stats.byProvider).length} provider${Object.keys(stats.byProvider).length !== 1 ? "s" : ""}` : "Centralized AI assets for your team" })
171
+ /* @__PURE__ */ jsx(Box, { sx: { fontSize: "0.85rem", fontWeight: 400, color: "rgba(255,255,255,0.75)", lineHeight: 1 }, children: stats ? (() => {
172
+ const providerCount = Object.keys(stats.byProvider).length;
173
+ const providerStr = t(providerCount === 1 ? "devAiHubPage.providerCountOne" : "devAiHubPage.providerCountOther", { n: String(providerCount) });
174
+ return t("devAiHubPage.totalStats", { totalAssets: String(stats.totalAssets), providers: providerStr });
175
+ })() : t("devAiHubPage.subtitle") })
160
176
  ] })
161
177
  ] }),
162
178
  pageTitleOverride: "Dev AI Hub",
@@ -179,80 +195,121 @@ function DevAiHubPage() {
179
195
  children: /* @__PURE__ */ jsx(ToolIcon, { tool, branded: false, sx: { fontSize: "1rem", color: "#fff" } })
180
196
  }
181
197
  ) }, tool)) }),
182
- stats?.lastSync && /* @__PURE__ */ jsx(Tooltip, { title: `Last sync: ${new Date(stats.lastSync).toLocaleString()}`, arrow: true, children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.5, cursor: "default" }, children: [
198
+ stats?.lastSync && /* @__PURE__ */ jsx(Tooltip, { title: t("devAiHubPage.lastSync", { time: new Date(stats.lastSync).toLocaleString() }), arrow: true, children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.5, cursor: "default" }, children: [
183
199
  /* @__PURE__ */ jsx(FiberManualRecordIcon, { sx: { fontSize: "0.6rem", color: "#4ade80" } }),
184
- /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "rgba(255,255,255,0.8)", whiteSpace: "nowrap" }, children: timeAgo(stats.lastSync) })
200
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "rgba(255,255,255,0.8)", whiteSpace: "nowrap" }, children: timeAgo(stats.lastSync, t) })
185
201
  ] }) }),
186
- /* @__PURE__ */ jsx(
187
- Button,
188
- {
189
- variant: "contained",
190
- size: "small",
191
- startIcon: /* @__PURE__ */ jsx(SettingsEthernetIcon, {}),
192
- onClick: () => setMcpDialogOpen(true),
193
- sx: {
194
- borderRadius: 2,
195
- fontWeight: 700,
196
- whiteSpace: "nowrap",
197
- background: "rgba(255,255,255,0.15)",
198
- backdropFilter: "blur(4px)",
199
- border: "1px solid rgba(255,255,255,0.3)",
200
- color: "#fff",
201
- boxShadow: "none",
202
- "&:hover": {
203
- background: "rgba(255,255,255,0.25)",
204
- boxShadow: "none"
205
- }
206
- },
207
- children: "Configure MCP"
208
- }
209
- )
210
- ] })
211
- }
212
- ),
213
- /* @__PURE__ */ jsxs(Content, { children: [
214
- /* @__PURE__ */ jsx(Grid, { container: true, spacing: 2, sx: { mb: 3 }, children: STATS_CONFIG.map(({ key, label, Icon, gradient, shadow }) => /* @__PURE__ */ jsx(Grid, { item: true, xs: 6, sm: 3, children: /* @__PURE__ */ jsx(
215
- Box,
216
- {
217
- onClick: () => handleFiltersChange({ ...filters, types: filters.types[0] === key ? [] : [key] }),
218
- sx: {
219
- background: gradient,
220
- borderRadius: 3,
221
- p: 2,
222
- cursor: "pointer",
223
- boxShadow: filters.types[0] === key ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
224
- transform: filters.types[0] === key ? "translateY(-2px)" : "none",
225
- transition: "all 0.2s ease",
226
- outline: filters.types[0] === key ? "2px solid rgba(255,255,255,0.6)" : "none",
227
- outlineOffset: 2,
228
- "&:hover": {
229
- boxShadow: `0 8px 24px ${shadow}`,
230
- transform: "translateY(-2px)"
231
- }
232
- },
233
- children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start" }, children: [
234
- /* @__PURE__ */ jsxs(Box, { children: [
235
- /* @__PURE__ */ jsx(Typography, { variant: "h4", fontWeight: 800, sx: { color: "#fff", lineHeight: 1 }, children: stats ? stats.byType[key] ?? 0 : /* @__PURE__ */ jsx(Skeleton, { width: 32, sx: { bgcolor: "rgba(255,255,255,0.3)" } }) }),
236
- /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "rgba(255,255,255,0.85)", fontWeight: 500, mt: 0.5 }, children: label })
237
- ] }),
202
+ /* @__PURE__ */ jsxs(Box, { sx: { position: "relative", display: "inline-flex" }, children: [
238
203
  /* @__PURE__ */ jsx(
239
- Box,
204
+ Button,
240
205
  {
206
+ variant: "contained",
207
+ size: "small",
208
+ startIcon: /* @__PURE__ */ jsx(ExtensionIcon, {}),
209
+ onClick: () => setMcpDialogOpen(true),
241
210
  sx: {
242
- width: 40,
243
- height: 40,
244
211
  borderRadius: 2,
245
- backgroundColor: "rgba(255,255,255,0.2)",
246
- display: "flex",
247
- alignItems: "center",
248
- justifyContent: "center"
212
+ fontWeight: 700,
213
+ whiteSpace: "nowrap",
214
+ background: "rgba(255,255,255,0.2)",
215
+ backdropFilter: "blur(4px)",
216
+ border: "1.5px solid rgba(255,255,255,0.45)",
217
+ color: "#fff",
218
+ boxShadow: "none",
219
+ "&:hover": {
220
+ background: "rgba(255,255,255,0.32)",
221
+ borderColor: "rgba(255,255,255,0.7)",
222
+ boxShadow: "0 0 0 2px rgba(255,255,255,0.15)"
223
+ }
249
224
  },
250
- children: /* @__PURE__ */ jsx(Icon, { sx: { color: "#fff", fontSize: "1.4rem" } })
225
+ children: t("devAiHubPage.configMcp")
226
+ }
227
+ ),
228
+ catalog.length > 0 && /* @__PURE__ */ jsx(
229
+ Box,
230
+ {
231
+ sx: {
232
+ position: "absolute",
233
+ top: -3,
234
+ right: -3,
235
+ width: 10,
236
+ height: 10,
237
+ borderRadius: "50%",
238
+ bgcolor: "#4ade80",
239
+ boxShadow: "0 0 0 2px rgba(0,0,0,0.15)",
240
+ animation: "mcpPulse 2s ease-in-out infinite",
241
+ "@keyframes mcpPulse": {
242
+ "0%": { boxShadow: "0 0 0 0 rgba(74,222,128,0.7), 0 0 0 2px rgba(0,0,0,0.15)" },
243
+ "70%": { boxShadow: "0 0 0 6px rgba(74,222,128,0), 0 0 0 2px rgba(0,0,0,0.15)" },
244
+ "100%": { boxShadow: "0 0 0 0 rgba(74,222,128,0), 0 0 0 2px rgba(0,0,0,0.15)" }
245
+ }
246
+ }
251
247
  }
252
248
  )
253
249
  ] })
250
+ ] })
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsxs(Content, { children: [
254
+ /* @__PURE__ */ jsx(Grid, { container: true, spacing: 2, sx: { mb: 3 }, children: statsCards.map((key) => {
255
+ const { label, Icon } = STATS_META[key];
256
+ const color = typeColors[key];
257
+ const gradient = `linear-gradient(135deg, ${color} 0%, ${darken(color, 0.15)} 100%)`;
258
+ const shadow = `${color}40`;
259
+ const selected = filters.types[0] === key;
260
+ return /* @__PURE__ */ jsx(Grid, { item: true, xs: 6, sm: 3, children: /* @__PURE__ */ jsx(
261
+ Box,
262
+ {
263
+ onClick: () => handleFiltersChange({ ...filters, types: selected ? [] : [key] }),
264
+ sx: {
265
+ background: gradient,
266
+ borderRadius: 3,
267
+ p: 2,
268
+ cursor: "pointer",
269
+ boxShadow: selected ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
270
+ transform: selected ? "translateY(-2px)" : "none",
271
+ transition: "all 0.2s ease",
272
+ outline: selected ? "2px solid rgba(255,255,255,0.6)" : "none",
273
+ outlineOffset: 2,
274
+ "&:hover": { boxShadow: `0 8px 24px ${shadow}`, transform: "translateY(-2px)" }
275
+ },
276
+ children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start" }, children: [
277
+ /* @__PURE__ */ jsxs(Box, { children: [
278
+ /* @__PURE__ */ jsx(Typography, { variant: "h4", fontWeight: 800, sx: { color: "#fff", lineHeight: 1 }, children: stats ? stats.byType[key] ?? 0 : /* @__PURE__ */ jsx(Skeleton, { width: 32, sx: { bgcolor: "rgba(255,255,255,0.3)" } }) }),
279
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "rgba(255,255,255,0.85)", fontWeight: 500, mt: 0.5 }, children: label })
280
+ ] }),
281
+ /* @__PURE__ */ jsx(Box, { sx: {
282
+ width: 40,
283
+ height: 40,
284
+ borderRadius: 2,
285
+ backgroundColor: "rgba(255,255,255,0.2)",
286
+ display: "flex",
287
+ alignItems: "center",
288
+ justifyContent: "center"
289
+ }, children: /* @__PURE__ */ jsx(Icon, { sx: { color: "#fff", fontSize: "1.4rem" } }) })
290
+ ] })
291
+ }
292
+ ) }, key);
293
+ }) }),
294
+ providers.length > 0 && /* @__PURE__ */ jsx(Box, { sx: { display: "flex", justifyContent: "flex-end", mb: 1, mt: -1 }, children: /* @__PURE__ */ jsx(
295
+ Tooltip,
296
+ {
297
+ title: t(providers.length === 1 ? "devAiHubPage.providerCountOne" : "devAiHubPage.providerCountOther", { n: String(providers.length) }),
298
+ placement: "left",
299
+ children: /* @__PURE__ */ jsx(
300
+ IconButton,
301
+ {
302
+ size: "small",
303
+ onClick: () => setProvidersDrawerOpen(true),
304
+ sx: {
305
+ color: providers.some((p) => p.status === "error") ? "error.main" : "text.disabled",
306
+ "&:hover": { color: providers.some((p) => p.status === "error") ? "error.dark" : "text.secondary" }
307
+ },
308
+ children: /* @__PURE__ */ jsx(CloudSyncIcon, { fontSize: "small" })
309
+ }
310
+ )
254
311
  }
255
- ) }, key)) }),
312
+ ) }),
256
313
  /* @__PURE__ */ jsx(
257
314
  AssetFilters,
258
315
  {
@@ -262,24 +319,22 @@ function DevAiHubPage() {
262
319
  providers: providers.length > 1 ? providers : void 0
263
320
  }
264
321
  ),
265
- result && !loading && /* @__PURE__ */ jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { mb: 2, display: "block" }, children: [
266
- result.totalCount,
267
- " asset",
268
- result.totalCount !== 1 ? "s" : "",
269
- " found"
270
- ] }),
322
+ result && !loading && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", sx: { mb: 2, display: "block" }, children: t(result.totalCount === 1 ? "devAiHubPage.assetCountOne" : "devAiHubPage.assetCountOther", { n: String(result.totalCount) }) }),
271
323
  /* @__PURE__ */ jsx(Grid, { container: true, spacing: 1.5, children: loading ? Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, lg: 3, children: /* @__PURE__ */ jsx(Skeleton, { variant: "rectangular", height: 150, sx: { borderRadius: 2 } }) }, i)) : result?.items.map((asset) => /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, lg: 3, children: /* @__PURE__ */ jsx(
272
324
  AssetCard,
273
325
  {
274
326
  asset,
275
327
  onView: handleViewAsset,
276
- onInstall: handleInstallAsset
328
+ onInstall: handleInstallAsset,
329
+ onHelp: handleHelpAsset,
330
+ onOpenMcpCatalog: () => setMcpDialogOpen(true),
331
+ mcpCatalog: catalog
277
332
  }
278
333
  ) }, asset.id)) }),
279
334
  !loading && result?.items.length === 0 && /* @__PURE__ */ jsxs(Box, { sx: { py: 12, textAlign: "center" }, children: [
280
335
  /* @__PURE__ */ jsx(Typography, { variant: "h1", sx: { mb: 2, opacity: 0.15, fontSize: "5rem" }, children: "\u{1F916}" }),
281
- /* @__PURE__ */ jsx(Typography, { variant: "h6", color: "text.secondary", fontWeight: 600, children: "No assets found" }),
282
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.disabled", sx: { mt: 0.5 }, children: "Try adjusting your filters or search terms." })
336
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", color: "text.secondary", fontWeight: 600, children: t("devAiHubPage.noAssetsTitle") }),
337
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.disabled", sx: { mt: 0.5 }, children: t("devAiHubPage.noAssetsSubtitle") })
283
338
  ] }),
284
339
  totalPages > 1 && /* @__PURE__ */ jsx(Box, { sx: { display: "flex", justifyContent: "center", mt: 4 }, children: /* @__PURE__ */ jsx(
285
340
  Pagination,
@@ -292,25 +347,102 @@ function DevAiHubPage() {
292
347
  }
293
348
  ) })
294
349
  ] }),
350
+ /* @__PURE__ */ jsx(AssetDetailPanel, { assetId: selectedAssetId, onClose: handleCloseDetail }),
351
+ /* @__PURE__ */ jsx(AssetInstallDialog, { assetId: installAssetId, onClose: handleCloseInstall }),
295
352
  /* @__PURE__ */ jsx(
296
- AssetDetailPanel,
353
+ AssetHelpDialog,
297
354
  {
298
- assetId: selectedAssetId,
299
- onClose: handleCloseDetail
355
+ asset: helpAssetId ? result?.items.find((a) => a.id === helpAssetId) ?? null : null,
356
+ onClose: handleCloseHelp
300
357
  }
301
358
  ),
359
+ /* @__PURE__ */ jsx(McpConfigDialog, { open: mcpDialogOpen, onClose: () => setMcpDialogOpen(false) }),
302
360
  /* @__PURE__ */ jsx(
303
- AssetInstallDialog,
361
+ Snackbar,
304
362
  {
305
- assetId: installAssetId,
306
- onClose: handleCloseInstall
363
+ open: syncSnackbar,
364
+ autoHideDuration: 3e3,
365
+ onClose: () => setSyncSnackbar(false),
366
+ message: t("devAiHubPage.syncTriggered")
307
367
  }
308
368
  ),
309
369
  /* @__PURE__ */ jsx(
310
- McpConfigDialog,
370
+ Drawer,
311
371
  {
312
- open: mcpDialogOpen,
313
- onClose: () => setMcpDialogOpen(false)
372
+ anchor: "right",
373
+ open: providersDrawerOpen,
374
+ onClose: () => setProvidersDrawerOpen(false),
375
+ PaperProps: { sx: { width: { xs: "100vw", sm: 400 } } },
376
+ children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", height: "100%" }, children: [
377
+ /* @__PURE__ */ jsxs(
378
+ Box,
379
+ {
380
+ sx: {
381
+ p: 2,
382
+ display: "flex",
383
+ alignItems: "center",
384
+ justifyContent: "space-between",
385
+ borderBottom: 1,
386
+ borderColor: "divider"
387
+ },
388
+ children: [
389
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", fontWeight: 700, children: t("devAiHubPage.providersSectionTitle") }),
390
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
391
+ canSync && /* @__PURE__ */ jsx(
392
+ Button,
393
+ {
394
+ size: "small",
395
+ variant: "outlined",
396
+ startIcon: providers.some((p) => syncing[p.id]) ? /* @__PURE__ */ jsx(CircularProgress, { size: 14 }) : /* @__PURE__ */ jsx(SyncIcon, {}),
397
+ disabled: providers.some((p) => syncing[p.id]),
398
+ onClick: async () => {
399
+ await triggerSyncAll(providers.map((p) => p.id));
400
+ setSyncSnackbar(true);
401
+ },
402
+ children: t("devAiHubPage.syncAllButton")
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: () => setProvidersDrawerOpen(false), children: /* @__PURE__ */ jsx(CloseIcon, {}) })
406
+ ] })
407
+ ]
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1, overflow: "auto" }, children: providers.map((provider, idx) => /* @__PURE__ */ jsxs(Box, { children: [
411
+ /* @__PURE__ */ jsxs(Box, { sx: { px: 2, py: 1.75, display: "flex", alignItems: "center", gap: 1.5 }, children: [
412
+ provider.status === "error" && /* @__PURE__ */ jsx(Tooltip, { title: provider.error ?? t("devAiHubPage.providerStatusError"), children: /* @__PURE__ */ jsx(ErrorOutlineIcon, { sx: { fontSize: "1.1rem", color: "error.main", flexShrink: 0 } }) }),
413
+ provider.status === "syncing" && /* @__PURE__ */ jsx(CircularProgress, { size: 16, sx: { flexShrink: 0 } }),
414
+ provider.status !== "error" && provider.status !== "syncing" && /* @__PURE__ */ jsx(CheckCircleOutlineIcon, { sx: { fontSize: "1.1rem", color: "success.main", flexShrink: 0 } }),
415
+ /* @__PURE__ */ jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [
416
+ /* @__PURE__ */ jsx(
417
+ Typography,
418
+ {
419
+ variant: "body2",
420
+ fontWeight: 600,
421
+ noWrap: true,
422
+ title: provider.target,
423
+ sx: { fontFamily: "monospace", fontSize: "0.8rem" },
424
+ children: provider.target
425
+ }
426
+ ),
427
+ provider.lastSync && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.disabled", children: timeAgo(provider.lastSync, t) }),
428
+ provider.status === "error" && provider.error && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "error", sx: { display: "block" }, noWrap: true, title: provider.error, children: provider.error })
429
+ ] }),
430
+ canSync && /* @__PURE__ */ jsx(Tooltip, { title: t("devAiHubPage.syncButton"), children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(
431
+ IconButton,
432
+ {
433
+ size: "small",
434
+ disabled: !!syncing[provider.id],
435
+ onClick: async () => {
436
+ await triggerSync(provider.id);
437
+ setSyncSnackbar(true);
438
+ },
439
+ children: syncing[provider.id] ? /* @__PURE__ */ jsx(CircularProgress, { size: 16 }) : /* @__PURE__ */ jsx(SyncIcon, { fontSize: "small" })
440
+ }
441
+ ) }) })
442
+ ] }),
443
+ idx < providers.length - 1 && /* @__PURE__ */ jsx(Divider, {})
444
+ ] }, provider.id)) })
445
+ ] })
314
446
  }
315
447
  )
316
448
  ] });