@mimicai/explorer 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+ import type { AdapterInfo } from '@/lib/api';
4
+ import { RawJsonViewer } from '@/components/explorer/json-viewer';
5
+ import { groupEndpoints } from '@/lib/group-endpoints';
6
+
7
+ interface EndpointTesterProps {
8
+ adapter: AdapterInfo;
9
+ initialEndpoint?: { method: string; path: string };
10
+ }
11
+
12
+ const METHOD_COLORS: Record<string, string> = {
13
+ GET: 'bg-blue-500/10 text-blue-500 border-blue-500/30',
14
+ POST: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/30',
15
+ PUT: 'bg-amber-500/10 text-amber-500 border-amber-500/30',
16
+ PATCH: 'bg-orange-500/10 text-orange-500 border-orange-500/30',
17
+ DELETE: 'bg-red-500/10 text-red-500 border-red-500/30',
18
+ };
19
+
20
+ export function EndpointTester({ adapter, initialEndpoint }: EndpointTesterProps) {
21
+ const [selectedEndpoint, setSelectedEndpoint] = useState(
22
+ initialEndpoint ?? adapter.endpoints[0] ?? null,
23
+ );
24
+ const [requestBody, setRequestBody] = useState('{}');
25
+ const [response, setResponse] = useState<{
26
+ status: number;
27
+ headers: Record<string, string>;
28
+ body: unknown;
29
+ duration: number;
30
+ } | null>(null);
31
+ const [loading, setLoading] = useState(false);
32
+ const [error, setError] = useState<string | null>(null);
33
+ const [sidebarCollapsed, setSidebarCollapsed] = useState<Record<string, boolean>>({});
34
+ const groups = useMemo(() => groupEndpoints(adapter.endpoints, adapter.basePath), [adapter]);
35
+
36
+ // Find which group the initial/selected endpoint belongs to, keep that expanded
37
+ const initialGroup = useMemo(() => {
38
+ if (!selectedEndpoint) return null;
39
+ return groups.find((g) =>
40
+ g.endpoints.some((ep) => ep.method === selectedEndpoint.method && ep.path === selectedEndpoint.path),
41
+ )?.label ?? null;
42
+ }, []);
43
+
44
+ // Start with all groups collapsed except the one containing the selected endpoint
45
+ const [sidebarInited] = useState(() => {
46
+ const init: Record<string, boolean> = {};
47
+ for (const g of groups) {
48
+ init[g.label] = g.label !== initialGroup;
49
+ }
50
+ return init;
51
+ });
52
+
53
+ const getCollapsed = (label: string) => sidebarCollapsed[label] ?? sidebarInited[label] ?? true;
54
+ const toggleSidebar = (label: string) =>
55
+ setSidebarCollapsed((prev) => ({ ...prev, [label]: !getCollapsed(label) }));
56
+
57
+ const handleSend = async () => {
58
+ if (!selectedEndpoint || !adapter.port) return;
59
+
60
+ setLoading(true);
61
+ setError(null);
62
+ setResponse(null);
63
+
64
+ const start = performance.now();
65
+ try {
66
+ const res = await fetch(`http://localhost:${adapter.port}${selectedEndpoint.path}`, {
67
+ method: selectedEndpoint.method,
68
+ headers: { 'Content-Type': 'application/json' },
69
+ ...(selectedEndpoint.method !== 'GET' && requestBody.trim()
70
+ ? { body: requestBody }
71
+ : {}),
72
+ });
73
+ const duration = Math.round(performance.now() - start);
74
+ const body = await res.json().catch(() => null);
75
+ setResponse({
76
+ status: res.status,
77
+ headers: Object.fromEntries(res.headers.entries()),
78
+ body,
79
+ duration,
80
+ });
81
+ } catch (err) {
82
+ setError(
83
+ err instanceof Error ? err.message : 'Request failed. Is mimic host running?',
84
+ );
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+
90
+ return (
91
+ <div className="space-y-6">
92
+ <div>
93
+ <h1 className="text-2xl font-bold">{adapter.name} — Endpoint Tester</h1>
94
+ <p className="mt-1 text-muted-foreground">
95
+ Send requests to the running mock server on port {adapter.port ?? 'N/A'}
96
+ </p>
97
+ </div>
98
+
99
+ <div className="flex gap-6">
100
+ {/* Endpoint list (grouped) */}
101
+ <div className="w-72 shrink-0 space-y-0.5 overflow-y-auto max-h-[calc(100vh-12rem)]">
102
+ {groups.map(({ label, endpoints }) => {
103
+ const isCollapsed = getCollapsed(label);
104
+ const hasSelected = endpoints.some(
105
+ (ep) => ep.method === selectedEndpoint?.method && ep.path === selectedEndpoint?.path,
106
+ );
107
+ return (
108
+ <div key={label}>
109
+ <button
110
+ onClick={() => toggleSidebar(label)}
111
+ className={cn(
112
+ 'flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-xs transition-colors hover:bg-accent',
113
+ hasSelected && isCollapsed && 'text-accent-foreground',
114
+ )}
115
+ >
116
+ <span className={cn('text-[10px] text-muted-foreground transition-transform', !isCollapsed && 'rotate-90')}>
117
+
118
+ </span>
119
+ <span className="font-medium truncate">{label}</span>
120
+ <span className="ml-auto text-[10px] text-muted-foreground">{endpoints.length}</span>
121
+ </button>
122
+ {!isCollapsed &&
123
+ endpoints.map((ep, i) => (
124
+ <button
125
+ key={i}
126
+ onClick={() => {
127
+ setSelectedEndpoint(ep);
128
+ setResponse(null);
129
+ setError(null);
130
+ }}
131
+ className={cn(
132
+ 'flex w-full items-center gap-2 rounded-md px-3 pl-6 py-1.5 text-sm transition-colors',
133
+ 'hover:bg-accent',
134
+ selectedEndpoint?.path === ep.path && selectedEndpoint?.method === ep.method
135
+ ? 'bg-accent text-accent-foreground'
136
+ : 'text-muted-foreground',
137
+ )}
138
+ >
139
+ <span
140
+ className={cn(
141
+ 'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold',
142
+ METHOD_COLORS[ep.method] ?? 'bg-muted',
143
+ )}
144
+ >
145
+ {ep.method}
146
+ </span>
147
+ <span className="truncate font-mono text-xs">{ep.path}</span>
148
+ </button>
149
+ ))}
150
+ </div>
151
+ );
152
+ })}
153
+ </div>
154
+
155
+ {/* Request / Response */}
156
+ <div className="flex-1 space-y-4">
157
+ {selectedEndpoint && (
158
+ <>
159
+ {/* Request bar */}
160
+ <div className="flex items-center gap-3">
161
+ <span
162
+ className={cn(
163
+ 'shrink-0 rounded border px-3 py-1.5 text-sm font-bold',
164
+ METHOD_COLORS[selectedEndpoint.method] ?? 'bg-muted',
165
+ )}
166
+ >
167
+ {selectedEndpoint.method}
168
+ </span>
169
+ <div className="flex-1 rounded-md border bg-muted/50 px-3 py-1.5 font-mono text-sm">
170
+ http://localhost:{adapter.port}{selectedEndpoint.path}
171
+ </div>
172
+ <button
173
+ onClick={handleSend}
174
+ disabled={loading || !adapter.port}
175
+ className={cn(
176
+ 'rounded-md bg-primary px-6 py-1.5 text-sm font-medium text-primary-foreground',
177
+ 'hover:bg-primary/90 transition-colors',
178
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
179
+ )}
180
+ >
181
+ {loading ? 'Sending...' : 'Send'}
182
+ </button>
183
+ </div>
184
+
185
+ {/* Request body (for POST/PUT/PATCH) */}
186
+ {selectedEndpoint.method !== 'GET' &&
187
+ selectedEndpoint.method !== 'DELETE' && (
188
+ <div>
189
+ <div className="mb-2 text-xs font-medium text-muted-foreground">
190
+ Request Body
191
+ </div>
192
+ <textarea
193
+ value={requestBody}
194
+ onChange={(e) => setRequestBody(e.target.value)}
195
+ className="w-full rounded-md border bg-muted/30 p-3 font-mono text-sm resize-y min-h-[100px] focus:outline-none focus:ring-1 focus:ring-ring"
196
+ placeholder="{}"
197
+ />
198
+ </div>
199
+ )}
200
+
201
+ {/* Error */}
202
+ {error && (
203
+ <div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-500">
204
+ {error}
205
+ </div>
206
+ )}
207
+
208
+ {/* Response */}
209
+ {response && (
210
+ <div className="space-y-3">
211
+ <div className="flex items-center gap-4">
212
+ <span
213
+ className={cn(
214
+ 'rounded px-2 py-0.5 text-sm font-bold',
215
+ response.status < 300
216
+ ? 'bg-emerald-500/10 text-emerald-500'
217
+ : response.status < 500
218
+ ? 'bg-amber-500/10 text-amber-500'
219
+ : 'bg-red-500/10 text-red-500',
220
+ )}
221
+ >
222
+ {response.status}
223
+ </span>
224
+ <span className="text-xs text-muted-foreground">
225
+ {response.duration}ms
226
+ </span>
227
+ </div>
228
+ <div className="rounded-lg border bg-card overflow-auto max-h-[600px] p-4">
229
+ <RawJsonViewer data={response.body} />
230
+ </div>
231
+ </div>
232
+ )}
233
+ </>
234
+ )}
235
+ </div>
236
+ </div>
237
+ </div>
238
+ );
239
+ }
@@ -0,0 +1,49 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ darkMode: 'class',
5
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
6
+ theme: {
7
+ extend: {
8
+ colors: {
9
+ border: 'hsl(var(--border))',
10
+ input: 'hsl(var(--input))',
11
+ ring: 'hsl(var(--ring))',
12
+ background: 'hsl(var(--background))',
13
+ foreground: 'hsl(var(--foreground))',
14
+ primary: {
15
+ DEFAULT: 'hsl(var(--primary))',
16
+ foreground: 'hsl(var(--primary-foreground))',
17
+ },
18
+ secondary: {
19
+ DEFAULT: 'hsl(var(--secondary))',
20
+ foreground: 'hsl(var(--secondary-foreground))',
21
+ },
22
+ muted: {
23
+ DEFAULT: 'hsl(var(--muted))',
24
+ foreground: 'hsl(var(--muted-foreground))',
25
+ },
26
+ accent: {
27
+ DEFAULT: 'hsl(var(--accent))',
28
+ foreground: 'hsl(var(--accent-foreground))',
29
+ },
30
+ destructive: {
31
+ DEFAULT: 'hsl(var(--destructive))',
32
+ foreground: 'hsl(var(--destructive-foreground))',
33
+ },
34
+ card: {
35
+ DEFAULT: 'hsl(var(--card))',
36
+ foreground: 'hsl(var(--card-foreground))',
37
+ },
38
+ },
39
+ borderRadius: {
40
+ lg: 'var(--radius)',
41
+ md: 'calc(var(--radius) - 2px)',
42
+ sm: 'calc(var(--radius) - 4px)',
43
+ },
44
+ },
45
+ },
46
+ plugins: [],
47
+ };
48
+
49
+ export default config;
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "outDir": "dist",
17
+ "isolatedModules": true,
18
+ "paths": {
19
+ "@/*": ["./src/*"]
20
+ }
21
+ },
22
+ "include": ["src", "server"]
23
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['server/index.ts'],
5
+ format: ['esm'],
6
+ outDir: 'dist/server',
7
+ dts: true,
8
+ clean: false,
9
+ target: 'node22',
10
+ external: ['@mimicai/core', 'fastify', '@fastify/static', '@fastify/cors'],
11
+ });
package/vite.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { resolve } from 'node:path';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ root: '.',
8
+ build: {
9
+ outDir: 'dist/client',
10
+ emptyDirOnly: true,
11
+ },
12
+ resolve: {
13
+ alias: {
14
+ '@': resolve(__dirname, './src'),
15
+ },
16
+ },
17
+ server: {
18
+ proxy: {
19
+ '/_api': 'http://localhost:7879',
20
+ },
21
+ },
22
+ });