@qwickapps/server 1.3.0 → 1.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 (241) hide show
  1. package/README.md +311 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +144 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/core/types.d.ts +19 -0
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  16. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  18. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  19. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  20. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  22. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  23. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  24. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  25. package/dist/plugins/auth/adapters/index.js +1 -0
  26. package/dist/plugins/auth/adapters/index.js.map +1 -1
  27. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  28. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  29. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  30. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  31. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  32. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  33. package/dist/plugins/auth/config-store.d.ts +11 -0
  34. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  35. package/dist/plugins/auth/config-store.js +232 -0
  36. package/dist/plugins/auth/config-store.js.map +1 -0
  37. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  38. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  39. package/dist/plugins/auth/config-store.test.js +299 -0
  40. package/dist/plugins/auth/config-store.test.js.map +1 -0
  41. package/dist/plugins/auth/env-config.d.ts +138 -0
  42. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  43. package/dist/plugins/auth/env-config.js +1122 -0
  44. package/dist/plugins/auth/env-config.js.map +1 -0
  45. package/dist/plugins/auth/index.d.ts +7 -1
  46. package/dist/plugins/auth/index.d.ts.map +1 -1
  47. package/dist/plugins/auth/index.js +7 -0
  48. package/dist/plugins/auth/index.js.map +1 -1
  49. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  50. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  51. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  52. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  53. package/dist/plugins/auth/types.d.ts +176 -0
  54. package/dist/plugins/auth/types.d.ts.map +1 -1
  55. package/dist/plugins/auth/types.js.map +1 -1
  56. package/dist/plugins/cache-plugin.test.js +3 -0
  57. package/dist/plugins/cache-plugin.test.js.map +1 -1
  58. package/dist/plugins/index.d.ts +6 -2
  59. package/dist/plugins/index.d.ts.map +1 -1
  60. package/dist/plugins/index.js +5 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/postgres-plugin.test.js +3 -0
  63. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  64. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  65. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  66. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  67. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  68. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  69. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  70. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  71. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  72. package/dist/plugins/preferences/index.d.ts +12 -0
  73. package/dist/plugins/preferences/index.d.ts.map +1 -0
  74. package/dist/plugins/preferences/index.js +13 -0
  75. package/dist/plugins/preferences/index.js.map +1 -0
  76. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  77. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  78. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  79. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  80. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  81. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  82. package/dist/plugins/preferences/stores/index.js +9 -0
  83. package/dist/plugins/preferences/stores/index.js.map +1 -0
  84. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  85. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  86. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  87. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  88. package/dist/plugins/preferences/types.d.ts +91 -0
  89. package/dist/plugins/preferences/types.d.ts.map +1 -0
  90. package/dist/plugins/preferences/types.js +10 -0
  91. package/dist/plugins/preferences/types.js.map +1 -0
  92. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  93. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  94. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  95. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  96. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  97. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  98. package/dist/plugins/rate-limit/cleanup.js +72 -0
  99. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  100. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  101. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  102. package/dist/plugins/rate-limit/env-config.js +318 -0
  103. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  104. package/dist/plugins/rate-limit/index.d.ts +76 -0
  105. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  106. package/dist/plugins/rate-limit/index.js +79 -0
  107. package/dist/plugins/rate-limit/index.js.map +1 -0
  108. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  109. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  110. package/dist/plugins/rate-limit/middleware.js +169 -0
  111. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  112. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  113. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  114. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  115. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  116. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  117. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  118. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  119. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  120. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  121. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  122. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  123. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  124. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  125. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  126. package/dist/plugins/rate-limit/stores/index.js +8 -0
  127. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  128. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  129. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  130. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  131. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  132. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  133. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  134. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  135. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  136. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  137. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  138. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  139. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  140. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  141. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  142. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  143. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  144. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  145. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  146. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  147. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  148. package/dist/plugins/rate-limit/types.d.ts +265 -0
  149. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  150. package/dist/plugins/rate-limit/types.js +9 -0
  151. package/dist/plugins/rate-limit/types.js.map +1 -0
  152. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  153. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  154. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  155. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  156. package/dist/plugins/users/index.d.ts +2 -2
  157. package/dist/plugins/users/index.d.ts.map +1 -1
  158. package/dist/plugins/users/index.js +1 -1
  159. package/dist/plugins/users/index.js.map +1 -1
  160. package/dist/plugins/users/types.d.ts +36 -0
  161. package/dist/plugins/users/types.d.ts.map +1 -1
  162. package/dist/plugins/users/users-plugin.d.ts +8 -2
  163. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  164. package/dist/plugins/users/users-plugin.js +122 -5
  165. package/dist/plugins/users/users-plugin.js.map +1 -1
  166. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  167. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  168. package/dist-ui/index.html +1 -1
  169. package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
  170. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  171. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  172. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  173. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  174. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  175. package/dist-ui-lib/index.js +3665 -3945
  176. package/dist-ui-lib/index.js.map +1 -1
  177. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  178. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  179. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  180. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  181. package/package.json +7 -2
  182. package/src/core/control-panel.ts +161 -2
  183. package/src/core/plugin-registry.ts +63 -0
  184. package/src/core/types.ts +17 -0
  185. package/src/index.ts +45 -0
  186. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  187. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  188. package/src/plugins/auth/adapters/index.ts +1 -0
  189. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  190. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  191. package/src/plugins/auth/config-store.test.ts +417 -0
  192. package/src/plugins/auth/config-store.ts +305 -0
  193. package/src/plugins/auth/env-config.ts +1279 -0
  194. package/src/plugins/auth/index.ts +30 -0
  195. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  196. package/src/plugins/auth/types.ts +218 -0
  197. package/src/plugins/cache-plugin.test.ts +3 -0
  198. package/src/plugins/index.ts +75 -0
  199. package/src/plugins/postgres-plugin.test.ts +3 -0
  200. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  201. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  202. package/src/plugins/preferences/index.ts +30 -0
  203. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  204. package/src/plugins/preferences/stores/index.ts +9 -0
  205. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  206. package/src/plugins/preferences/types.ts +100 -0
  207. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  208. package/src/plugins/rate-limit/cleanup.ts +117 -0
  209. package/src/plugins/rate-limit/env-config.ts +400 -0
  210. package/src/plugins/rate-limit/index.ts +128 -0
  211. package/src/plugins/rate-limit/middleware.ts +212 -0
  212. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  213. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  214. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  215. package/src/plugins/rate-limit/stores/index.ts +8 -0
  216. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  217. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  218. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  219. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  220. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  221. package/src/plugins/rate-limit/types.ts +338 -0
  222. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  223. package/src/plugins/users/index.ts +3 -0
  224. package/src/plugins/users/types.ts +38 -0
  225. package/src/plugins/users/users-plugin.ts +142 -5
  226. package/ui/src/App.tsx +35 -14
  227. package/ui/src/api/controlPanelApi.ts +326 -1
  228. package/ui/src/components/ControlPanelApp.tsx +3 -0
  229. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  230. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  231. package/ui/src/dashboard/builtInWidgets.tsx +13 -3
  232. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  233. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  234. package/ui/src/dashboard/widgets/index.ts +2 -0
  235. package/ui/src/pages/AuthPage.tsx +1103 -0
  236. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  237. package/ui/src/pages/PluginsPage.tsx +394 -0
  238. package/ui/src/pages/RateLimitPage.tsx +292 -0
  239. package/ui/vite.lib.config.ts +5 -0
  240. package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
  241. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1,288 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Button,
8
+ CircularProgress,
9
+ Alert,
10
+ IconButton,
11
+ Tooltip,
12
+ Chip,
13
+ LinearProgress,
14
+ } from '@mui/material';
15
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
16
+ import ErrorIcon from '@mui/icons-material/Error';
17
+ import RefreshIcon from '@mui/icons-material/Refresh';
18
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
19
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
20
+ import { api } from '../api/controlPanelApi';
21
+
22
+ interface Integration {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ configured: boolean;
27
+ apiKey: string;
28
+ docsUrl: string;
29
+ }
30
+
31
+ interface IntegrationsConfig {
32
+ integrations: Integration[];
33
+ stats: {
34
+ totalRequests: number;
35
+ };
36
+ }
37
+
38
+ interface TestResult {
39
+ success: boolean;
40
+ message: string;
41
+ latency?: number;
42
+ }
43
+
44
+ export function IntegrationsPage() {
45
+ const [config, setConfig] = useState<IntegrationsConfig | null>(null);
46
+ const [loading, setLoading] = useState(true);
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [testingId, setTestingId] = useState<string | null>(null);
49
+ const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
50
+
51
+ const fetchConfig = useCallback(async () => {
52
+ setLoading(true);
53
+ setError(null);
54
+ try {
55
+ const data = await api.fetch<IntegrationsConfig>('/ai-proxy/config');
56
+ setConfig(data);
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : 'Failed to fetch integrations config');
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ fetchConfig();
66
+ }, [fetchConfig]);
67
+
68
+ const handleTest = async (integrationId: string) => {
69
+ setTestingId(integrationId);
70
+ setTestResults((prev) => ({ ...prev, [integrationId]: { success: false, message: 'Testing...' } }));
71
+
72
+ try {
73
+ const result = await api.fetch<TestResult>(`/ai-proxy/test/${integrationId}`, {
74
+ method: 'POST',
75
+ });
76
+ setTestResults((prev) => ({ ...prev, [integrationId]: result }));
77
+ } catch (err) {
78
+ setTestResults((prev) => ({
79
+ ...prev,
80
+ [integrationId]: {
81
+ success: false,
82
+ message: err instanceof Error ? err.message : 'Test failed',
83
+ },
84
+ }));
85
+ } finally {
86
+ setTestingId(null);
87
+ }
88
+ };
89
+
90
+ if (loading) {
91
+ return (
92
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
93
+ <CircularProgress />
94
+ </Box>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <Box>
100
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
101
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
102
+ AI Integrations
103
+ </Typography>
104
+ <Tooltip title="Refresh">
105
+ <IconButton onClick={fetchConfig} sx={{ color: 'var(--theme-text-secondary)' }}>
106
+ <RefreshIcon />
107
+ </IconButton>
108
+ </Tooltip>
109
+ </Box>
110
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
111
+ Manage AI service connections for QwickBot
112
+ </Typography>
113
+
114
+ {error && (
115
+ <Alert severity="error" sx={{ mb: 3 }} onClose={() => setError(null)}>
116
+ {error}
117
+ </Alert>
118
+ )}
119
+
120
+ {/* Stats Card */}
121
+ {config && (
122
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
123
+ <CardContent>
124
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
125
+ <Box>
126
+ <Typography variant="h3" sx={{ color: 'var(--theme-primary)', fontWeight: 600 }}>
127
+ {config.stats.totalRequests}
128
+ </Typography>
129
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
130
+ Total API Requests
131
+ </Typography>
132
+ </Box>
133
+ <Box sx={{ borderLeft: '1px solid var(--theme-border)', pl: 3 }}>
134
+ <Typography variant="h3" sx={{ color: 'var(--theme-success)', fontWeight: 600 }}>
135
+ {config.integrations.filter((i) => i.configured).length}
136
+ </Typography>
137
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
138
+ Configured Services
139
+ </Typography>
140
+ </Box>
141
+ </Box>
142
+ </CardContent>
143
+ </Card>
144
+ )}
145
+
146
+ {/* Integration Cards */}
147
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
148
+ {config?.integrations.map((integration) => {
149
+ const testResult = testResults[integration.id];
150
+ const isTesting = testingId === integration.id;
151
+
152
+ return (
153
+ <Card key={integration.id} sx={{ bgcolor: 'var(--theme-surface)' }}>
154
+ <CardContent>
155
+ {/* Header */}
156
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
157
+ <Box>
158
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
159
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
160
+ {integration.name}
161
+ </Typography>
162
+ <Chip
163
+ label={integration.configured ? 'Configured' : 'Not Configured'}
164
+ size="small"
165
+ sx={{
166
+ bgcolor: integration.configured ? 'var(--theme-success)20' : 'var(--theme-error)20',
167
+ color: integration.configured ? 'var(--theme-success)' : 'var(--theme-error)',
168
+ fontWeight: 500,
169
+ }}
170
+ />
171
+ </Box>
172
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
173
+ {integration.description}
174
+ </Typography>
175
+ </Box>
176
+ {integration.configured ? (
177
+ <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />
178
+ ) : (
179
+ <ErrorIcon sx={{ color: 'var(--theme-error)' }} />
180
+ )}
181
+ </Box>
182
+
183
+ {/* API Key */}
184
+ <Box sx={{ mb: 2, p: 1.5, bgcolor: 'var(--theme-background)', borderRadius: 1 }}>
185
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)', display: 'block', mb: 0.5 }}>
186
+ API Key
187
+ </Typography>
188
+ <Typography
189
+ sx={{
190
+ fontFamily: 'monospace',
191
+ fontSize: 13,
192
+ color: integration.configured ? 'var(--theme-text-primary)' : 'var(--theme-text-secondary)',
193
+ }}
194
+ >
195
+ {integration.apiKey}
196
+ </Typography>
197
+ </Box>
198
+
199
+ {/* Test Result */}
200
+ {testResult && (
201
+ <Box sx={{ mb: 2 }}>
202
+ {isTesting ? (
203
+ <LinearProgress sx={{ height: 4, borderRadius: 2 }} />
204
+ ) : (
205
+ <Alert
206
+ severity={testResult.success ? 'success' : 'error'}
207
+ sx={{ py: 0.5, '& .MuiAlert-message': { fontSize: 13 } }}
208
+ >
209
+ {testResult.message}
210
+ {testResult.latency !== undefined && (
211
+ <Typography variant="caption" sx={{ display: 'block', mt: 0.5 }}>
212
+ Latency: {testResult.latency}ms
213
+ </Typography>
214
+ )}
215
+ </Alert>
216
+ )}
217
+ </Box>
218
+ )}
219
+
220
+ {/* Actions */}
221
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
222
+ <Button
223
+ size="small"
224
+ startIcon={<OpenInNewIcon />}
225
+ href={integration.docsUrl}
226
+ target="_blank"
227
+ sx={{
228
+ color: 'var(--theme-text-secondary)',
229
+ '&:hover': { color: 'var(--theme-primary)' },
230
+ }}
231
+ >
232
+ Docs
233
+ </Button>
234
+ <Button
235
+ size="small"
236
+ variant="outlined"
237
+ startIcon={isTesting ? <CircularProgress size={14} /> : <PlayArrowIcon />}
238
+ onClick={() => handleTest(integration.id)}
239
+ disabled={!integration.configured || isTesting}
240
+ sx={{
241
+ borderColor: 'var(--theme-border)',
242
+ color: 'var(--theme-text-primary)',
243
+ '&:hover': { borderColor: 'var(--theme-primary)' },
244
+ }}
245
+ >
246
+ Test Connection
247
+ </Button>
248
+ </Box>
249
+ </CardContent>
250
+ </Card>
251
+ );
252
+ })}
253
+ </Box>
254
+
255
+ {/* Setup Instructions */}
256
+ {config && config.integrations.some((i) => !i.configured) && (
257
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mt: 3 }}>
258
+ <CardContent>
259
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
260
+ Setup Instructions
261
+ </Typography>
262
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)', mb: 2 }}>
263
+ Add the following environment variables to configure the AI services:
264
+ </Typography>
265
+ <Box
266
+ component="pre"
267
+ sx={{
268
+ p: 2,
269
+ bgcolor: 'var(--theme-background)',
270
+ borderRadius: 1,
271
+ overflow: 'auto',
272
+ fontFamily: 'monospace',
273
+ fontSize: 13,
274
+ color: 'var(--theme-text-primary)',
275
+ }}
276
+ >
277
+ {`# Groq - for chat (https://console.groq.com)
278
+ GROQ_API_KEY=your_groq_api_key
279
+
280
+ # Gemini - for vision (https://ai.google.dev)
281
+ GEMINI_API_KEY=your_gemini_api_key`}
282
+ </Box>
283
+ </CardContent>
284
+ </Card>
285
+ )}
286
+ </Box>
287
+ );
288
+ }
@@ -0,0 +1,394 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ CircularProgress,
8
+ Chip,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableContainer,
13
+ TableHead,
14
+ TableRow,
15
+ Collapse,
16
+ IconButton,
17
+ Alert,
18
+ } from '@mui/material';
19
+ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
20
+ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
21
+ import ExtensionIcon from '@mui/icons-material/Extension';
22
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
23
+ import ErrorIcon from '@mui/icons-material/Error';
24
+ import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
25
+ import StopCircleIcon from '@mui/icons-material/StopCircle';
26
+ import { api, PluginInfo, PluginContributions } from '../api/controlPanelApi';
27
+
28
+ function getStatusIcon(status: PluginInfo['status']) {
29
+ switch (status) {
30
+ case 'active':
31
+ return <CheckCircleIcon sx={{ color: 'var(--theme-success)', fontSize: 20 }} />;
32
+ case 'error':
33
+ return <ErrorIcon sx={{ color: 'var(--theme-error)', fontSize: 20 }} />;
34
+ case 'starting':
35
+ return <HourglassEmptyIcon sx={{ color: 'var(--theme-warning)', fontSize: 20 }} />;
36
+ case 'stopped':
37
+ return <StopCircleIcon sx={{ color: 'var(--theme-text-secondary)', fontSize: 20 }} />;
38
+ default:
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function getStatusColor(status: PluginInfo['status']): string {
44
+ switch (status) {
45
+ case 'active':
46
+ return 'var(--theme-success)';
47
+ case 'error':
48
+ return 'var(--theme-error)';
49
+ case 'starting':
50
+ return 'var(--theme-warning)';
51
+ case 'stopped':
52
+ return 'var(--theme-text-secondary)';
53
+ default:
54
+ return 'var(--theme-text-secondary)';
55
+ }
56
+ }
57
+
58
+ function ContributionsSummary({ counts }: { counts: PluginInfo['contributionCounts'] }) {
59
+ const parts: string[] = [];
60
+ if (counts.routes > 0) parts.push(`${counts.routes} route${counts.routes > 1 ? 's' : ''}`);
61
+ if (counts.menuItems > 0) parts.push(`${counts.menuItems} menu item${counts.menuItems > 1 ? 's' : ''}`);
62
+ if (counts.pages > 0) parts.push(`${counts.pages} page${counts.pages > 1 ? 's' : ''}`);
63
+ if (counts.widgets > 0) parts.push(`${counts.widgets} widget${counts.widgets > 1 ? 's' : ''}`);
64
+
65
+ if (parts.length === 0) {
66
+ return <Typography sx={{ color: 'var(--theme-text-secondary)', fontSize: '0.875rem' }}>No contributions</Typography>;
67
+ }
68
+
69
+ return (
70
+ <Typography sx={{ color: 'var(--theme-text-secondary)', fontSize: '0.875rem' }}>
71
+ {parts.join(', ')}
72
+ </Typography>
73
+ );
74
+ }
75
+
76
+ interface PluginRowProps {
77
+ plugin: PluginInfo;
78
+ }
79
+
80
+ function PluginRow({ plugin }: PluginRowProps) {
81
+ const [open, setOpen] = useState(false);
82
+ const [contributions, setContributions] = useState<PluginContributions | null>(null);
83
+ const [loading, setLoading] = useState(false);
84
+ const [detailError, setDetailError] = useState<string | null>(null);
85
+
86
+ const handleToggle = async () => {
87
+ if (!open && !contributions && !detailError) {
88
+ setLoading(true);
89
+ setDetailError(null);
90
+ try {
91
+ const detail = await api.getPluginDetail(plugin.id);
92
+ setContributions(detail.contributions);
93
+ } catch (err) {
94
+ console.error('Failed to load plugin details:', err);
95
+ setDetailError(err instanceof Error ? err.message : 'Failed to load details');
96
+ } finally {
97
+ setLoading(false);
98
+ }
99
+ }
100
+ setOpen(!open);
101
+ };
102
+
103
+ const hasContributions =
104
+ plugin.contributionCounts.routes > 0 ||
105
+ plugin.contributionCounts.menuItems > 0 ||
106
+ plugin.contributionCounts.pages > 0 ||
107
+ plugin.contributionCounts.widgets > 0;
108
+
109
+ return (
110
+ <>
111
+ <TableRow
112
+ sx={{
113
+ '& > *': { borderBottom: open ? 'none' : undefined },
114
+ cursor: hasContributions ? 'pointer' : 'default',
115
+ '&:hover': { bgcolor: hasContributions ? 'var(--theme-background)' : undefined },
116
+ }}
117
+ onClick={hasContributions ? handleToggle : undefined}
118
+ >
119
+ <TableCell sx={{ width: 50, borderColor: 'var(--theme-border)' }}>
120
+ {hasContributions && (
121
+ <IconButton size="small" sx={{ color: 'var(--theme-text-secondary)' }}>
122
+ {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
123
+ </IconButton>
124
+ )}
125
+ </TableCell>
126
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
127
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
128
+ {getStatusIcon(plugin.status)}
129
+ <Typography sx={{ color: 'var(--theme-text-primary)', fontWeight: 500 }}>
130
+ {plugin.name}
131
+ </Typography>
132
+ </Box>
133
+ </TableCell>
134
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
135
+ <Typography sx={{ color: 'var(--theme-text-secondary)', fontFamily: 'monospace', fontSize: '0.875rem' }}>
136
+ {plugin.id}
137
+ </Typography>
138
+ </TableCell>
139
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
140
+ {plugin.version ? (
141
+ <Chip
142
+ label={`v${plugin.version}`}
143
+ size="small"
144
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
145
+ />
146
+ ) : (
147
+ <Typography sx={{ color: 'var(--theme-text-secondary)' }}>-</Typography>
148
+ )}
149
+ </TableCell>
150
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
151
+ <Chip
152
+ label={plugin.status}
153
+ size="small"
154
+ sx={{
155
+ bgcolor: getStatusColor(plugin.status) + '20',
156
+ color: getStatusColor(plugin.status),
157
+ textTransform: 'capitalize',
158
+ }}
159
+ />
160
+ </TableCell>
161
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
162
+ <ContributionsSummary counts={plugin.contributionCounts} />
163
+ </TableCell>
164
+ </TableRow>
165
+
166
+ {/* Expanded details row */}
167
+ <TableRow>
168
+ <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6} sx={{ borderColor: 'var(--theme-border)' }}>
169
+ <Collapse in={open} timeout="auto" unmountOnExit>
170
+ <Box sx={{ py: 2, px: 4 }}>
171
+ {loading ? (
172
+ <Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
173
+ <CircularProgress size={24} />
174
+ </Box>
175
+ ) : detailError ? (
176
+ <Alert severity="error" sx={{ mb: 1 }}>
177
+ {detailError}
178
+ </Alert>
179
+ ) : contributions ? (
180
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
181
+ {/* Error message if plugin failed */}
182
+ {plugin.status === 'error' && plugin.error && (
183
+ <Alert severity="error" sx={{ mb: 1 }}>
184
+ {plugin.error}
185
+ </Alert>
186
+ )}
187
+
188
+ {/* Routes */}
189
+ {contributions.routes.length > 0 && (
190
+ <Box>
191
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
192
+ Routes ({contributions.routes.length})
193
+ </Typography>
194
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
195
+ {contributions.routes.map((route, i) => (
196
+ <Chip
197
+ key={i}
198
+ label={`${route.method.toUpperCase()} ${route.path}`}
199
+ size="small"
200
+ sx={{
201
+ bgcolor: 'var(--theme-background)',
202
+ color: 'var(--theme-text-primary)',
203
+ fontFamily: 'monospace',
204
+ fontSize: '0.75rem',
205
+ }}
206
+ />
207
+ ))}
208
+ </Box>
209
+ </Box>
210
+ )}
211
+
212
+ {/* Menu Items */}
213
+ {contributions.menuItems.length > 0 && (
214
+ <Box>
215
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
216
+ Menu Items ({contributions.menuItems.length})
217
+ </Typography>
218
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
219
+ {contributions.menuItems.map((item) => (
220
+ <Chip
221
+ key={item.id}
222
+ label={`${item.label} (${item.route})`}
223
+ size="small"
224
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
225
+ />
226
+ ))}
227
+ </Box>
228
+ </Box>
229
+ )}
230
+
231
+ {/* Pages */}
232
+ {contributions.pages.length > 0 && (
233
+ <Box>
234
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
235
+ Pages ({contributions.pages.length})
236
+ </Typography>
237
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
238
+ {contributions.pages.map((page) => (
239
+ <Chip
240
+ key={page.id}
241
+ label={`${page.title || page.id} (${page.route})`}
242
+ size="small"
243
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
244
+ />
245
+ ))}
246
+ </Box>
247
+ </Box>
248
+ )}
249
+
250
+ {/* Widgets */}
251
+ {contributions.widgets.length > 0 && (
252
+ <Box>
253
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
254
+ Widgets ({contributions.widgets.length})
255
+ </Typography>
256
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
257
+ {contributions.widgets.map((widget) => (
258
+ <Chip
259
+ key={widget.id}
260
+ label={widget.title}
261
+ size="small"
262
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
263
+ />
264
+ ))}
265
+ </Box>
266
+ </Box>
267
+ )}
268
+ </Box>
269
+ ) : null}
270
+ </Box>
271
+ </Collapse>
272
+ </TableCell>
273
+ </TableRow>
274
+ </>
275
+ );
276
+ }
277
+
278
+ export function PluginsPage() {
279
+ const [plugins, setPlugins] = useState<PluginInfo[]>([]);
280
+ const [loading, setLoading] = useState(true);
281
+ const [error, setError] = useState<string | null>(null);
282
+
283
+ useEffect(() => {
284
+ const fetchPlugins = async () => {
285
+ try {
286
+ const data = await api.getPlugins();
287
+ setPlugins(data.plugins);
288
+ setError(null);
289
+ } catch (err) {
290
+ setError(err instanceof Error ? err.message : 'Failed to fetch plugins');
291
+ } finally {
292
+ setLoading(false);
293
+ }
294
+ };
295
+
296
+ fetchPlugins();
297
+ }, []);
298
+
299
+ if (loading) {
300
+ return (
301
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
302
+ <CircularProgress />
303
+ </Box>
304
+ );
305
+ }
306
+
307
+ if (error) {
308
+ return (
309
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
310
+ <CardContent>
311
+ <Typography color="error">{error}</Typography>
312
+ </CardContent>
313
+ </Card>
314
+ );
315
+ }
316
+
317
+ // Count plugins by status
318
+ const activeCount = plugins.filter((p) => p.status === 'active').length;
319
+ const errorCount = plugins.filter((p) => p.status === 'error').length;
320
+
321
+ return (
322
+ <Box>
323
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
324
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
325
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
326
+ Plugins
327
+ </Typography>
328
+ <Chip
329
+ icon={<ExtensionIcon sx={{ fontSize: 16 }} />}
330
+ label={`${activeCount}/${plugins.length} active`}
331
+ size="small"
332
+ sx={{
333
+ bgcolor: activeCount === plugins.length ? 'var(--theme-success)20' : 'var(--theme-warning)20',
334
+ color: activeCount === plugins.length ? 'var(--theme-success)' : 'var(--theme-warning)',
335
+ }}
336
+ />
337
+ {errorCount > 0 && (
338
+ <Chip
339
+ icon={<ErrorIcon sx={{ fontSize: 16 }} />}
340
+ label={`${errorCount} error${errorCount > 1 ? 's' : ''}`}
341
+ size="small"
342
+ sx={{ bgcolor: 'var(--theme-error)20', color: 'var(--theme-error)' }}
343
+ />
344
+ )}
345
+ </Box>
346
+ </Box>
347
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
348
+ View registered plugins and their contributions to the control panel
349
+ </Typography>
350
+
351
+ {plugins.length === 0 ? (
352
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
353
+ <CardContent>
354
+ <Typography sx={{ color: 'var(--theme-text-secondary)', textAlign: 'center', py: 4 }}>
355
+ No plugins registered
356
+ </Typography>
357
+ </CardContent>
358
+ </Card>
359
+ ) : (
360
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
361
+ <TableContainer>
362
+ <Table>
363
+ <TableHead>
364
+ <TableRow>
365
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 50 }} />
366
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
367
+ Name
368
+ </TableCell>
369
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
370
+ ID
371
+ </TableCell>
372
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
373
+ Version
374
+ </TableCell>
375
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
376
+ Status
377
+ </TableCell>
378
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
379
+ Contributions
380
+ </TableCell>
381
+ </TableRow>
382
+ </TableHead>
383
+ <TableBody>
384
+ {plugins.map((plugin) => (
385
+ <PluginRow key={plugin.id} plugin={plugin} />
386
+ ))}
387
+ </TableBody>
388
+ </Table>
389
+ </TableContainer>
390
+ </Card>
391
+ )}
392
+ </Box>
393
+ );
394
+ }