@cybermem/dashboard 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/.dockerignore +11 -0
- package/.eslintrc.json +3 -0
- package/Dockerfile +48 -0
- package/app/api/audit-logs/route.ts +60 -0
- package/app/api/metrics/route.ts +141 -0
- package/app/api/prometheus/route.ts +65 -0
- package/app/api/settings/regenerate/route.ts +20 -0
- package/app/api/settings/route.ts +25 -0
- package/app/api/system/restart/route.ts +18 -0
- package/app/globals.css +148 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +150 -0
- package/components/dashboard/audit-log-table.tsx +195 -0
- package/components/dashboard/chart-card.tsx +196 -0
- package/components/dashboard/charts-section.tsx +16 -0
- package/components/dashboard/header.tsx +82 -0
- package/components/dashboard/login-modal.tsx +87 -0
- package/components/dashboard/mcp-config-modal.tsx +397 -0
- package/components/dashboard/metric-card.tsx +23 -0
- package/components/dashboard/metrics-chart.tsx +134 -0
- package/components/dashboard/metrics-grid.tsx +136 -0
- package/components/dashboard/settings-modal.tsx +345 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/calendar.tsx +213 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +135 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +104 -0
- package/components/ui/field.tsx +244 -0
- package/components/ui/form.tsx +167 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +169 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/item.tsx +193 -0
- package/components/ui/kbd.tsx +28 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +166 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +56 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +185 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/sonner.tsx +25 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/toast.tsx +129 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.tsx +19 -0
- package/components/ui/use-toast.ts +191 -0
- package/components.json +21 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +191 -0
- package/lib/data/dashboard-context.tsx +75 -0
- package/lib/data/demo-strategy.ts +110 -0
- package/lib/data/production-strategy.ts +152 -0
- package/lib/data/types.ts +52 -0
- package/lib/prometheus/client.ts +58 -0
- package/lib/prometheus/index.ts +6 -0
- package/lib/prometheus/metrics.ts +234 -0
- package/lib/prometheus/sparklines.ts +71 -0
- package/lib/prometheus/timeseries.ts +305 -0
- package/lib/prometheus/utils.ts +176 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +36 -0
- package/package.json +91 -0
- package/postcss.config.mjs +8 -0
- package/public/clients.json +165 -0
- package/public/favicon-dark.svg +1 -0
- package/public/favicon-light.svg +1 -0
- package/public/icons/antigravity.png +0 -0
- package/public/icons/chatgpt.png +0 -0
- package/public/icons/claude-code.png +0 -0
- package/public/icons/claude.png +0 -0
- package/public/icons/codex.png +0 -0
- package/public/icons/cursor.png +0 -0
- package/public/icons/gemini.png +0 -0
- package/public/icons/images.jpeg +0 -0
- package/public/icons/mcp.png +0 -0
- package/public/icons/mono.png +0 -0
- package/public/icons/perplexity.png +0 -0
- package/public/icons/vscode.png +0 -0
- package/public/icons/warp.png +0 -0
- package/public/icons/windsurf.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +7 -0
- package/public/manifest.json +21 -0
- package/public/site.webmanifest +21 -0
- package/public/web-app-manifest-192x192.png +0 -0
- package/public/web-app-manifest-512x512.png +0 -0
- package/shared.env +0 -0
- package/styles/globals.css +125 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import { Input } from "@/components/ui/input"
|
|
5
|
+
import { Label } from "@/components/ui/label"
|
|
6
|
+
import { Lock, LogIn } from "lucide-react"
|
|
7
|
+
import { useState } from "react"
|
|
8
|
+
|
|
9
|
+
interface LoginModalProps {
|
|
10
|
+
onLogin: (password: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function LoginModal({ onLogin }: LoginModalProps) {
|
|
14
|
+
const [password, setPassword] = useState("")
|
|
15
|
+
const [error, setError] = useState("")
|
|
16
|
+
|
|
17
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
|
|
20
|
+
// Get admin password from localStorage or use default
|
|
21
|
+
const adminPassword = localStorage.getItem("adminPassword") || "admin"
|
|
22
|
+
|
|
23
|
+
if (password === adminPassword) {
|
|
24
|
+
onLogin(password)
|
|
25
|
+
setError("")
|
|
26
|
+
} else {
|
|
27
|
+
setError("Incorrect password")
|
|
28
|
+
setPassword("")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="fixed inset-0 bg-black/80 backdrop-blur-md flex items-center justify-center z-50 p-4">
|
|
34
|
+
<div
|
|
35
|
+
className="bg-[#0B1116]/90 backdrop-blur-xl border border-emerald-500/20 rounded-2xl shadow-2xl max-w-md w-full overflow-hidden"
|
|
36
|
+
style={{
|
|
37
|
+
backgroundImage: `
|
|
38
|
+
radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%),
|
|
39
|
+
radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
|
|
40
|
+
radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
|
|
41
|
+
radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
|
|
42
|
+
`
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{/* Header */}
|
|
46
|
+
<div className="px-6 pt-8 pb-6 text-center">
|
|
47
|
+
<div className="inline-flex p-3 bg-emerald-500/10 rounded-full border border-emerald-500/20 mb-4">
|
|
48
|
+
<Lock className="w-8 h-8 text-emerald-400 drop-shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
|
49
|
+
</div>
|
|
50
|
+
<h2 className="text-2xl font-bold text-white text-shadow-sm mb-2">CyberMem Dashboard</h2>
|
|
51
|
+
<p className="text-neutral-400 text-sm">Enter admin password to continue</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Content */}
|
|
55
|
+
<form onSubmit={handleSubmit} className="px-6 pb-6 space-y-4">
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<Label htmlFor="password" className="text-neutral-200">Password</Label>
|
|
58
|
+
<Input
|
|
59
|
+
id="password"
|
|
60
|
+
type="password"
|
|
61
|
+
value={password}
|
|
62
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
63
|
+
placeholder="Enter admin password"
|
|
64
|
+
className="bg-black/40 border-white/10 text-white focus-visible:border-emerald-500/30 focus-visible:ring-emerald-500/10 placeholder:text-neutral-600 shadow-inner"
|
|
65
|
+
autoFocus
|
|
66
|
+
/>
|
|
67
|
+
{error && (
|
|
68
|
+
<p className="text-red-400 text-sm mt-2">{error}</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
type="submit"
|
|
74
|
+
className="w-full bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 font-medium transition-colors gap-2"
|
|
75
|
+
>
|
|
76
|
+
<LogIn className="w-4 h-4" />
|
|
77
|
+
Login
|
|
78
|
+
</Button>
|
|
79
|
+
|
|
80
|
+
<p className="text-xs text-neutral-500 text-center mt-4">
|
|
81
|
+
Default password: <code className="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">admin</code>
|
|
82
|
+
</p>
|
|
83
|
+
</form>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import { Input } from "@/components/ui/input"
|
|
5
|
+
import { Label } from "@/components/ui/label"
|
|
6
|
+
import { useDashboard } from "@/lib/data/dashboard-context"
|
|
7
|
+
import { Check, Copy, Eye, EyeOff, FileCode, Info, Monitor, X } from "lucide-react"
|
|
8
|
+
import { useEffect, useState } from "react"
|
|
9
|
+
|
|
10
|
+
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
11
|
+
const { clientConfigs } = useDashboard()
|
|
12
|
+
const clients = clientConfigs
|
|
13
|
+
const [selectedClient, setSelectedClient] = useState("claude")
|
|
14
|
+
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
15
|
+
const [apiKey, setApiKey] = useState("")
|
|
16
|
+
const [baseUrl, setBaseUrl] = useState("http://localhost:8080")
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
18
|
+
const [isKeyVisible, setIsKeyVisible] = useState(false)
|
|
19
|
+
const [showRegenConfirm, setShowRegenConfirm] = useState(false)
|
|
20
|
+
const [regenInputValue, setRegenInputValue] = useState("")
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Try to get key from local storage first (simulating persistence)
|
|
24
|
+
const localKey = localStorage.getItem("om_api_key")
|
|
25
|
+
if (localKey) {
|
|
26
|
+
setApiKey(localKey)
|
|
27
|
+
// We still fetch settings for the endpoint
|
|
28
|
+
fetch("/api/settings")
|
|
29
|
+
.then(res => res.json())
|
|
30
|
+
.then(data => {
|
|
31
|
+
let srvEndpoint = data.endpoint
|
|
32
|
+
if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
|
|
33
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:8080`
|
|
34
|
+
}
|
|
35
|
+
setBaseUrl(srvEndpoint)
|
|
36
|
+
setIsLoading(false)
|
|
37
|
+
})
|
|
38
|
+
.catch(err => setIsLoading(false))
|
|
39
|
+
} else {
|
|
40
|
+
fetch("/api/settings")
|
|
41
|
+
.then(res => res.json())
|
|
42
|
+
.then(data => {
|
|
43
|
+
setApiKey(data.apiKey !== 'not-set' ? data.apiKey : '')
|
|
44
|
+
let srvEndpoint = data.endpoint
|
|
45
|
+
if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
|
|
46
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:8080`
|
|
47
|
+
}
|
|
48
|
+
setBaseUrl(srvEndpoint)
|
|
49
|
+
setIsLoading(false)
|
|
50
|
+
})
|
|
51
|
+
.catch(err => {
|
|
52
|
+
console.error("Failed to fetch settings:", err)
|
|
53
|
+
setIsLoading(false)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const generateApiKey = () => {
|
|
59
|
+
// Legacy - redirected to confirmRegenerate logic via UI state
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const confirmRegenerate = async () => {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch('/api/settings/regenerate', { method: 'POST' })
|
|
65
|
+
if (!res.ok) throw new Error('Failed to regenerate key')
|
|
66
|
+
const data = await res.json()
|
|
67
|
+
|
|
68
|
+
const newKey = data.apiKey
|
|
69
|
+
setApiKey(newKey)
|
|
70
|
+
localStorage.setItem("om_api_key", newKey)
|
|
71
|
+
setIsKeyVisible(true)
|
|
72
|
+
setShowRegenConfirm(false)
|
|
73
|
+
setRegenInputValue("")
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(e)
|
|
76
|
+
alert("Failed to regenerate key on server.")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const getMcpConfig = (clientId: string) => {
|
|
81
|
+
const isAntigravity = clientId === 'antigravity'
|
|
82
|
+
const urlKey = isAntigravity ? 'serverUrl' : 'url'
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
mcpServers: {
|
|
86
|
+
cybermem: {
|
|
87
|
+
[urlKey]: `${baseUrl}/mcp`,
|
|
88
|
+
"headers": {
|
|
89
|
+
"x-api-key": apiKey || "sk-your-generated-key"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getConfigContent = (maskKey = false) => {
|
|
97
|
+
const config = (clients as any[]).find(c => c.id === selectedClient);
|
|
98
|
+
const displayKey = maskKey ? "••••••••••••••••" : (apiKey || "sk-your-generated-key");
|
|
99
|
+
const actualKey = apiKey || "sk-your-generated-key";
|
|
100
|
+
|
|
101
|
+
// Handle TOML config (Codex)
|
|
102
|
+
if (config?.configType === 'toml') {
|
|
103
|
+
return `# CyberMem Configuration\n[mcp]\nserver_url = "${baseUrl}/mcp"\napi_key = "${maskKey ? displayKey : actualKey}"`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle command-based configs (Claude Code, Gemini CLI, etc.)
|
|
107
|
+
if ((config?.configType === 'command' || config?.configType === 'cmd') && config?.command) {
|
|
108
|
+
let cmd = config.command.replace("http://localhost:8080", baseUrl);
|
|
109
|
+
|
|
110
|
+
// Add API key as header if it's missing and it's a CLI that usually supports it
|
|
111
|
+
if (config.id === 'claude-code' || config.id === 'gemini-cli') {
|
|
112
|
+
const headerPart = `--header "x-api-key: ${maskKey ? displayKey : actualKey}"`;
|
|
113
|
+
if (!cmd.includes('x-api-key')) {
|
|
114
|
+
// Insert before transport or just at the end
|
|
115
|
+
cmd = cmd.replace('mcp add', `mcp add ${headerPart}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return cmd;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Default to JSON config
|
|
122
|
+
const jsonConfig = getMcpConfig(selectedClient);
|
|
123
|
+
if (maskKey) {
|
|
124
|
+
(jsonConfig.mcpServers.cybermem as any).headers["x-api-key"] = displayKey;
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(jsonConfig, null, 2);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const copyToClipboard = (text: string, id: string) => {
|
|
130
|
+
navigator.clipboard.writeText(text)
|
|
131
|
+
setCopiedId(id)
|
|
132
|
+
setTimeout(() => setCopiedId(null), 2000)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const highlightJSON = (obj: any) => {
|
|
136
|
+
const json = JSON.stringify(obj, null, 2)
|
|
137
|
+
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
|
|
138
|
+
let cls = "text-orange-300"
|
|
139
|
+
if (/^"/.test(match)) {
|
|
140
|
+
if (/:$/.test(match)) {
|
|
141
|
+
cls = "text-emerald-300"
|
|
142
|
+
} else {
|
|
143
|
+
cls = "text-yellow-200"
|
|
144
|
+
}
|
|
145
|
+
} else if (/true|false/.test(match)) {
|
|
146
|
+
cls = "text-blue-300"
|
|
147
|
+
} else if (/null/.test(match)) {
|
|
148
|
+
cls = "text-gray-400"
|
|
149
|
+
}
|
|
150
|
+
return `<span class="${cls}">${match}</span>`
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const selectedConfig = (clients as any[]).find(c => c.id === selectedClient)
|
|
155
|
+
|
|
156
|
+
const renderInstructions = () => {
|
|
157
|
+
if (!selectedConfig) return null
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="space-y-4 text-sm text-neutral-300">
|
|
161
|
+
<p>{selectedConfig.description}</p>
|
|
162
|
+
{selectedConfig.steps.length > 0 && (
|
|
163
|
+
<ol className="list-decimal list-inside space-y-2 ml-2 text-neutral-400">
|
|
164
|
+
{selectedConfig.steps.map((step: string, i: number) => (
|
|
165
|
+
<li key={i} dangerouslySetInnerHTML={{
|
|
166
|
+
__html: step
|
|
167
|
+
.replace(/\*\*(.*?)\*\*/g, '<span class="text-white font-medium">$1</span>')
|
|
168
|
+
.replace(/`([^`]+)`/g, '<code class="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">$1</code>')
|
|
169
|
+
}} />
|
|
170
|
+
))}
|
|
171
|
+
</ol>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const configContent = getConfigContent()
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
|
181
|
+
<div
|
|
182
|
+
className="w-full max-w-4xl bg-[#0B1116]/80 backdrop-blur-xl border border-emerald-500/20 rounded-2xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[85vh] relative overflow-hidden"
|
|
183
|
+
style={{
|
|
184
|
+
backgroundImage: `
|
|
185
|
+
radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%),
|
|
186
|
+
radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
|
|
187
|
+
radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
|
|
188
|
+
radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
|
|
189
|
+
`
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{/* Header */}
|
|
193
|
+
<div className="flex items-center justify-between px-6 pt-6 pb-2 flex-none">
|
|
194
|
+
<div className="flex items-center gap-3">
|
|
195
|
+
<div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
|
|
196
|
+
<img src="/icons/mcp.png" alt="MCP Logo" className="w-5 h-5 drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
|
|
197
|
+
</div>
|
|
198
|
+
<h2 className="text-xl font-semibold text-white text-shadow-sm">Integrate MCP Client</h2>
|
|
199
|
+
</div>
|
|
200
|
+
<Button
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="icon"
|
|
203
|
+
onClick={onClose}
|
|
204
|
+
className="text-neutral-400 hover:text-white hover:bg-white/10 rounded-full"
|
|
205
|
+
>
|
|
206
|
+
<X className="w-5 h-5" />
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Content */}
|
|
211
|
+
<div className="p-6 space-y-6 overflow-y-auto flex-1 min-h-0">
|
|
212
|
+
|
|
213
|
+
{/* Client Selector */}
|
|
214
|
+
<section>
|
|
215
|
+
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
216
|
+
<Monitor className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
217
|
+
Select Client
|
|
218
|
+
</h3>
|
|
219
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
220
|
+
{clients.map((client) => (
|
|
221
|
+
<button
|
|
222
|
+
key={client.id}
|
|
223
|
+
onClick={() => setSelectedClient(client.id)}
|
|
224
|
+
className={`
|
|
225
|
+
relative group flex flex-col items-center justify-center p-3 rounded-xl transition-all duration-300 border
|
|
226
|
+
${selectedClient === client.id
|
|
227
|
+
? "bg-emerald-500/10 border-emerald-500/50 shadow-[0_0_15px_rgba(16,185,129,0.1)] backdrop-blur-sm"
|
|
228
|
+
: "bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/20 backdrop-blur-sm"
|
|
229
|
+
}
|
|
230
|
+
`}
|
|
231
|
+
>
|
|
232
|
+
<div className="mb-2 transition-transform duration-300 group-hover:scale-110">
|
|
233
|
+
{client.icon ? (
|
|
234
|
+
<img src={client.icon} alt={client.name} className="w-8 h-8 object-contain drop-shadow-lg" />
|
|
235
|
+
) : (
|
|
236
|
+
<div className="w-8 h-8 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 transition-transform duration-300">
|
|
237
|
+
<span className="text-sm font-bold">?</span>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
<span className={`text-[10px] font-medium text-center transition-colors ${selectedClient === client.id ? "text-emerald-400 text-shadow-emerald" : "text-neutral-400 group-hover:text-white"}`}>
|
|
242
|
+
{client.name}
|
|
243
|
+
</span>
|
|
244
|
+
</button>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</section>
|
|
248
|
+
|
|
249
|
+
{/* Instructions */}
|
|
250
|
+
<section>
|
|
251
|
+
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
252
|
+
<FileCode className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
253
|
+
{selectedClient === "codex" ? "Configuration" : (selectedClient === "other" ? "Configuration JSON" : "Integration Instructions")}
|
|
254
|
+
</h3>
|
|
255
|
+
|
|
256
|
+
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
|
|
257
|
+
{selectedClient === "chatgpt" && (
|
|
258
|
+
<div className="px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-200">
|
|
259
|
+
<p>Requires Developer Mode. <a href="https://platform.openai.com/docs/guides/developer-mode" target="_blank" rel="noopener noreferrer" className="underline hover:text-white">Read OpenAI Documentation</a></p>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{renderInstructions()}
|
|
264
|
+
|
|
265
|
+
{/* API Key Control Row (Standardized) */}
|
|
266
|
+
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm mb-4">
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
<Label htmlFor="mcp-api-key" className="text-neutral-200">Master API Key</Label>
|
|
269
|
+
<div className="flex gap-2">
|
|
270
|
+
<div className="relative flex-1">
|
|
271
|
+
<Input
|
|
272
|
+
id="mcp-api-key"
|
|
273
|
+
value={apiKey || "sk-not-generated-yet"}
|
|
274
|
+
readOnly
|
|
275
|
+
className="bg-black/40 border-white/10 text-white focus-visible:border-emerald-500/30 focus-visible:ring-emerald-500/10 placeholder:text-neutral-600 shadow-inner pr-10 font-mono"
|
|
276
|
+
type={isKeyVisible ? "text" : "password"}
|
|
277
|
+
/>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
onClick={() => setIsKeyVisible(!isKeyVisible)}
|
|
281
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
|
|
282
|
+
>
|
|
283
|
+
{isKeyVisible ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
<Button
|
|
287
|
+
size="icon"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
className="h-10 w-10 border border-white/10 bg-white/5 hover:bg-white/10 text-neutral-400 hover:text-white"
|
|
290
|
+
onClick={() => copyToClipboard(apiKey, "apikey")}
|
|
291
|
+
title="Copy API Key"
|
|
292
|
+
>
|
|
293
|
+
{copiedId === "apikey" ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Regeneration Controls */}
|
|
298
|
+
<div className="flex justify-end pt-2">
|
|
299
|
+
{showRegenConfirm ? (
|
|
300
|
+
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-4 duration-200">
|
|
301
|
+
<span className="text-xs text-red-400 font-medium">Warning: Disconnects clients.</span>
|
|
302
|
+
<Input
|
|
303
|
+
value={regenInputValue}
|
|
304
|
+
onChange={(e) => setRegenInputValue(e.target.value)}
|
|
305
|
+
placeholder="Type 'agree'"
|
|
306
|
+
className="h-8 w-28 bg-red-500/10 border-red-500/30 text-red-200 text-xs placeholder:text-red-500/30 focus-visible:border-red-500/50"
|
|
307
|
+
/>
|
|
308
|
+
<Button
|
|
309
|
+
size="sm"
|
|
310
|
+
variant="ghost"
|
|
311
|
+
className="h-8 px-3 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
312
|
+
onClick={() => {
|
|
313
|
+
setShowRegenConfirm(false)
|
|
314
|
+
setRegenInputValue("")
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
Cancel
|
|
318
|
+
</Button>
|
|
319
|
+
<Button
|
|
320
|
+
size="sm"
|
|
321
|
+
disabled={regenInputValue !== 'agree'}
|
|
322
|
+
className="h-8 px-3 bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
323
|
+
onClick={confirmRegenerate}
|
|
324
|
+
>
|
|
325
|
+
Confirm
|
|
326
|
+
</Button>
|
|
327
|
+
</div>
|
|
328
|
+
) : (
|
|
329
|
+
<Button
|
|
330
|
+
size="sm"
|
|
331
|
+
variant="ghost"
|
|
332
|
+
className="h-8 px-2 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
333
|
+
onClick={() => setShowRegenConfirm(true)}
|
|
334
|
+
>
|
|
335
|
+
Regenerate Key
|
|
336
|
+
</Button>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="relative group">
|
|
343
|
+
<div className="relative pl-5 py-5 pr-24 rounded-lg bg-[#0F161C] border border-white/10 font-mono text-xs md:text-sm text-white overflow-x-auto shadow-[0_0_20px_rgba(0,0,0,0.3)] inset-shadow">
|
|
344
|
+
<pre className="text-shadow-sm">
|
|
345
|
+
{(() => {
|
|
346
|
+
const config = (clients as any[]).find(c => c.id === selectedClient);
|
|
347
|
+
if (config?.configType === 'json') {
|
|
348
|
+
return <code dangerouslySetInnerHTML={{ __html: highlightJSON(JSON.parse(getConfigContent(!isKeyVisible))) }} />;
|
|
349
|
+
} else {
|
|
350
|
+
return getConfigContent(!isKeyVisible);
|
|
351
|
+
}
|
|
352
|
+
})()}
|
|
353
|
+
</pre>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="absolute top-5 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
356
|
+
<Button
|
|
357
|
+
size="sm"
|
|
358
|
+
variant="ghost"
|
|
359
|
+
className="h-8 px-3 text-white bg-black/40 backdrop-blur border border-white/5 shadow-[0_0_10px_rgba(255,255,255,0.05)] hover:bg-white/10 hover:text-white font-medium"
|
|
360
|
+
onClick={() => copyToClipboard(getConfigContent(false), "config")}
|
|
361
|
+
>
|
|
362
|
+
{copiedId === "config" ? <Check className="h-4 w-4 stroke-[2.5] text-emerald-400 mr-2 drop-shadow-[0_0_5px_rgba(52,211,153,0.5)]" /> : <Copy className="h-4 w-4 stroke-[2.5] mr-2 text-white drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />}
|
|
363
|
+
{copiedId === "config" ? <span className="text-emerald-400 text-shadow-sm">Copied</span> : <span className="text-white text-shadow-sm">Copy</span>}
|
|
364
|
+
</Button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div className="flex items-start gap-3 p-3 rounded-lg bg-emerald-500/5 border border-emerald-500/10 text-emerald-200/70 text-xs">
|
|
369
|
+
<Info className="h-4 w-4 shrink-0 text-white mt-0.5" />
|
|
370
|
+
<p>
|
|
371
|
+
This configuration includes your generated API key. Keep it secure and do not share it publicly.
|
|
372
|
+
</p>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{/* Footer */}
|
|
379
|
+
<div className="border-t border-emerald-500/20 px-6 py-4 flex justify-end gap-3 flex-none bg-[#0B1116]/30">
|
|
380
|
+
<Button
|
|
381
|
+
asChild
|
|
382
|
+
variant="ghost"
|
|
383
|
+
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
|
|
384
|
+
>
|
|
385
|
+
<a href="https://cybermem.dev/docs" target="_blank">Read Documentation</a>
|
|
386
|
+
</Button>
|
|
387
|
+
<Button
|
|
388
|
+
onClick={onClose}
|
|
389
|
+
className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
|
|
390
|
+
>
|
|
391
|
+
Close
|
|
392
|
+
</Button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from "@/components/ui/card"
|
|
4
|
+
|
|
5
|
+
interface MetricCardProps {
|
|
6
|
+
label: string
|
|
7
|
+
value: string
|
|
8
|
+
// Keep these props for API compatibility but don't use them
|
|
9
|
+
change?: string
|
|
10
|
+
trend?: "up" | "down" | "neutral"
|
|
11
|
+
hasData?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function MetricCard({ label, value }: MetricCardProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative">
|
|
17
|
+
<CardContent className="p-6 relative z-10">
|
|
18
|
+
<div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
|
|
19
|
+
<div className="text-4xl font-bold text-white">{value}</div>
|
|
20
|
+
</CardContent>
|
|
21
|
+
</Card>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
4
|
+
|
|
5
|
+
// Fallback color generator
|
|
6
|
+
function stringToColor(str: string): string {
|
|
7
|
+
let hash = 0;
|
|
8
|
+
for (let i = 0; i < str.length; i++) {
|
|
9
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
10
|
+
}
|
|
11
|
+
let color = '#';
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
const value = (hash >> (i * 8)) & 0xFF;
|
|
14
|
+
color += ('00' + value.toString(16)).substr(-2);
|
|
15
|
+
}
|
|
16
|
+
return color;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MetricsChartProps {
|
|
20
|
+
data: any[]
|
|
21
|
+
isMultiSeries: boolean
|
|
22
|
+
clientNames: string[]
|
|
23
|
+
clientConfigs: any[]
|
|
24
|
+
hovered: string | null
|
|
25
|
+
setHovered: (id: string | null) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function MetricsChart({
|
|
29
|
+
data,
|
|
30
|
+
isMultiSeries,
|
|
31
|
+
clientNames,
|
|
32
|
+
clientConfigs,
|
|
33
|
+
hovered,
|
|
34
|
+
setHovered
|
|
35
|
+
}: MetricsChartProps) {
|
|
36
|
+
return (
|
|
37
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
38
|
+
<AreaChart data={data} margin={{ top: 10, right: 20, left: 0, bottom: 0 }}>
|
|
39
|
+
<CartesianGrid strokeDasharray="0" stroke="#2D3135" opacity={0.3} vertical={false} horizontal={true} />
|
|
40
|
+
<XAxis
|
|
41
|
+
dataKey="time"
|
|
42
|
+
stroke="#6B7280"
|
|
43
|
+
fontSize={11}
|
|
44
|
+
tickLine={false}
|
|
45
|
+
axisLine={false}
|
|
46
|
+
minTickGap={40}
|
|
47
|
+
tick={{ fill: '#6B7280' }}
|
|
48
|
+
/>
|
|
49
|
+
<YAxis
|
|
50
|
+
stroke="#6B7280"
|
|
51
|
+
fontSize={11}
|
|
52
|
+
tickLine={false}
|
|
53
|
+
axisLine={false}
|
|
54
|
+
tickFormatter={(value) => `${value}`}
|
|
55
|
+
tick={{ fill: '#6B7280' }}
|
|
56
|
+
width={40}
|
|
57
|
+
/>
|
|
58
|
+
<Tooltip
|
|
59
|
+
contentStyle={{
|
|
60
|
+
backgroundColor: "rgba(11, 17, 22, 0.8)",
|
|
61
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
62
|
+
borderRadius: "12px",
|
|
63
|
+
color: "#fff",
|
|
64
|
+
backdropFilter: "blur(12px)",
|
|
65
|
+
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.5)"
|
|
66
|
+
}}
|
|
67
|
+
itemStyle={{ color: "#fff", fontSize: "12px", padding: "2px 0" }}
|
|
68
|
+
labelStyle={{ color: "#9ca3af", marginBottom: "8px", fontSize: "12px", fontWeight: 500 }}
|
|
69
|
+
cursor={{ stroke: 'rgba(255,255,255,0.2)', strokeWidth: 1 }}
|
|
70
|
+
/>
|
|
71
|
+
{isMultiSeries && (
|
|
72
|
+
<Legend
|
|
73
|
+
verticalAlign="bottom"
|
|
74
|
+
height={36}
|
|
75
|
+
iconType="circle"
|
|
76
|
+
onMouseEnter={(e: any) => {
|
|
77
|
+
if (e.dataKey) setHovered(e.dataKey.toString())
|
|
78
|
+
}}
|
|
79
|
+
onMouseLeave={() => setHovered(null)}
|
|
80
|
+
formatter={(value: any, entry: any) => {
|
|
81
|
+
// value is usually the name set on the Area, which we set below.
|
|
82
|
+
// But if that fails, we fallback to finding config.
|
|
83
|
+
return <span className="text-white">{value}</span>
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
{isMultiSeries ? (
|
|
88
|
+
clientNames.map((client, i) => {
|
|
89
|
+
// Find matching config
|
|
90
|
+
const keyLower = client.toLowerCase()
|
|
91
|
+
const config = clientConfigs.find((c: any) => keyLower.includes(c.match))
|
|
92
|
+
|
|
93
|
+
// Use config if found, otherwise fallback
|
|
94
|
+
const name = config?.name || client
|
|
95
|
+
const color = config?.color || stringToColor(client)
|
|
96
|
+
|
|
97
|
+
const isHovered = hovered === client
|
|
98
|
+
const isAnyHovered = hovered !== null
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Area
|
|
102
|
+
key={client}
|
|
103
|
+
type="monotone"
|
|
104
|
+
dataKey={client}
|
|
105
|
+
name={name}
|
|
106
|
+
stroke={color}
|
|
107
|
+
strokeWidth={isHovered ? 2.5 : 1.5}
|
|
108
|
+
fillOpacity={isHovered ? 0.5 : (isAnyHovered ? 0.1 : 0.2)}
|
|
109
|
+
fill={color}
|
|
110
|
+
stackId="1"
|
|
111
|
+
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
112
|
+
dot={false}
|
|
113
|
+
onMouseEnter={() => setHovered(client)}
|
|
114
|
+
onMouseLeave={() => setHovered(null)}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
) : (
|
|
119
|
+
<Area
|
|
120
|
+
type="monotone"
|
|
121
|
+
dataKey="value"
|
|
122
|
+
stroke="#10b981"
|
|
123
|
+
strokeWidth={1.5}
|
|
124
|
+
fillOpacity={0.2}
|
|
125
|
+
fill="#10b981"
|
|
126
|
+
activeDot={{ r: 3, strokeWidth: 0 }}
|
|
127
|
+
dot={false}
|
|
128
|
+
// No hover effect needed for single series as there's nothing to distinguish from
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</AreaChart>
|
|
132
|
+
</ResponsiveContainer>
|
|
133
|
+
)
|
|
134
|
+
}
|