@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,35 @@
1
+ type Endpoint = { method: string; path: string; description: string };
2
+
3
+ export interface EndpointGroup {
4
+ label: string;
5
+ endpoints: Endpoint[];
6
+ }
7
+
8
+ /**
9
+ * Groups endpoints by their top-level resource segment.
10
+ * e.g. /stripe/v1/customers/:id → "customers"
11
+ * /stripe/v1/accounts/:account/bank_accounts → "accounts"
12
+ */
13
+ export function groupEndpoints(endpoints: Endpoint[], basePath: string): EndpointGroup[] {
14
+ const map = new Map<string, Endpoint[]>();
15
+
16
+ for (const ep of endpoints) {
17
+ // Strip the basePath prefix, then extract the first segment
18
+ let rest = ep.path;
19
+ if (rest.startsWith(basePath)) {
20
+ rest = rest.slice(basePath.length);
21
+ }
22
+ // Remove leading slash and version prefix (e.g. "v1/")
23
+ rest = rest.replace(/^\//, '').replace(/^v\d+\//, '');
24
+ // First segment is the resource group
25
+ const seg = rest.split('/')[0] || 'other';
26
+ const key = seg.replace(/^:/, '');
27
+
28
+ if (!map.has(key)) map.set(key, []);
29
+ map.get(key)!.push(ep);
30
+ }
31
+
32
+ return Array.from(map.entries())
33
+ .sort(([a], [b]) => a.localeCompare(b))
34
+ .map(([label, endpoints]) => ({ label, endpoints }));
35
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,128 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+ import type { AdapterInfo } from '@/lib/api';
4
+ import type { Page } from '@/App';
5
+ import { groupEndpoints } from '@/lib/group-endpoints';
6
+
7
+ interface AdapterViewProps {
8
+ adapter: AdapterInfo;
9
+ onNavigate: (page: Page) => void;
10
+ }
11
+
12
+ const METHOD_COLORS: Record<string, string> = {
13
+ GET: 'bg-blue-500/10 text-blue-500',
14
+ POST: 'bg-emerald-500/10 text-emerald-500',
15
+ PUT: 'bg-amber-500/10 text-amber-500',
16
+ PATCH: 'bg-orange-500/10 text-orange-500',
17
+ DELETE: 'bg-red-500/10 text-red-500',
18
+ };
19
+
20
+ export function AdapterView({ adapter, onNavigate }: AdapterViewProps) {
21
+ const groups = useMemo(() => groupEndpoints(adapter.endpoints, adapter.basePath), [adapter]);
22
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
23
+
24
+ const toggle = (group: string) =>
25
+ setCollapsed((prev) => ({ ...prev, [group]: !prev[group] }));
26
+
27
+ return (
28
+ <div className="space-y-6">
29
+ {/* Header */}
30
+ <div className="flex items-start justify-between">
31
+ <div>
32
+ <h1 className="text-2xl font-bold">{adapter.name}</h1>
33
+ <p className="mt-1 text-muted-foreground">{adapter.description}</p>
34
+ <div className="mt-3 flex items-center gap-4 text-sm text-muted-foreground">
35
+ <span className="font-mono">@mimicai/adapter-{adapter.id}</span>
36
+ {adapter.port && <span>Port: {adapter.port}</span>}
37
+ {adapter.versions.length > 0 && (
38
+ <span>Versions: {adapter.versions.join(', ')}</span>
39
+ )}
40
+ </div>
41
+ </div>
42
+ <button
43
+ onClick={() => onNavigate({ type: 'tester', adapterId: adapter.id })}
44
+ className={cn(
45
+ 'rounded-md border bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
46
+ 'hover:bg-primary/90 transition-colors',
47
+ )}
48
+ >
49
+ Test Endpoints
50
+ </button>
51
+ </div>
52
+
53
+ {/* Endpoints grouped */}
54
+ <div>
55
+ <h2 className="mb-3 text-lg font-semibold">
56
+ Endpoints ({adapter.endpoints.length})
57
+ </h2>
58
+ <div className="space-y-2">
59
+ {groups.map(({ label, endpoints }) => {
60
+ const isCollapsed = collapsed[label] ?? false;
61
+ return (
62
+ <div key={label} className="rounded-lg border">
63
+ <button
64
+ onClick={() => toggle(label)}
65
+ className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-muted/30 transition-colors"
66
+ >
67
+ <span className={cn(
68
+ 'text-muted-foreground transition-transform text-xs',
69
+ isCollapsed ? '' : 'rotate-90',
70
+ )}>
71
+
72
+ </span>
73
+ <span className="font-medium text-sm">{label}</span>
74
+ <span className="ml-auto text-xs text-muted-foreground">
75
+ {endpoints.length} endpoint{endpoints.length !== 1 ? 's' : ''}
76
+ </span>
77
+ </button>
78
+ {!isCollapsed && (
79
+ <table className="w-full">
80
+ <tbody>
81
+ {endpoints.map((ep, i) => (
82
+ <tr
83
+ key={i}
84
+ className={cn(
85
+ 'transition-colors hover:bg-muted/30 border-t',
86
+ )}
87
+ >
88
+ <td className="px-4 py-2.5 w-24">
89
+ <span
90
+ className={cn(
91
+ 'inline-block rounded px-2 py-0.5 text-xs font-bold',
92
+ METHOD_COLORS[ep.method] ?? 'bg-muted text-muted-foreground',
93
+ )}
94
+ >
95
+ {ep.method}
96
+ </span>
97
+ </td>
98
+ <td className="px-4 py-2.5 font-mono text-sm">{ep.path}</td>
99
+ <td className="px-4 py-2.5 text-sm text-muted-foreground">
100
+ {ep.description}
101
+ </td>
102
+ <td className="px-4 py-2.5 text-right w-20">
103
+ <button
104
+ onClick={() =>
105
+ onNavigate({
106
+ type: 'tester',
107
+ adapterId: adapter.id,
108
+ endpoint: { method: ep.method, path: ep.path },
109
+ })
110
+ }
111
+ className="text-xs text-primary hover:underline"
112
+ >
113
+ Try it
114
+ </button>
115
+ </td>
116
+ </tr>
117
+ ))}
118
+ </tbody>
119
+ </table>
120
+ )}
121
+ </div>
122
+ );
123
+ })}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,134 @@
1
+ import { cn } from '@/lib/utils';
2
+ import type { ExplorerConfig, AdapterInfo, PersonaDataSummary } from '@/lib/api';
3
+ import type { Page } from '@/App';
4
+
5
+ interface DashboardProps {
6
+ config: ExplorerConfig;
7
+ adapters: AdapterInfo[];
8
+ dataSummary: PersonaDataSummary[];
9
+ onNavigate: (page: Page) => void;
10
+ }
11
+
12
+ export function Dashboard({ config, adapters, dataSummary, onNavigate }: DashboardProps) {
13
+ const totalEndpoints = adapters.reduce((s, a) => s + a.endpoints.length, 0);
14
+ const totalEntities = dataSummary.reduce(
15
+ (s, ps) =>
16
+ s +
17
+ Object.values(ps.tables).reduce((s2, n) => s2 + n, 0) +
18
+ Object.values(ps.apis).reduce(
19
+ (s2, resources) => s2 + Object.values(resources).reduce((s3, n) => s3 + n, 0),
20
+ 0,
21
+ ),
22
+ 0,
23
+ );
24
+
25
+ return (
26
+ <div className="space-y-8">
27
+ {/* Stats */}
28
+ <div className="grid grid-cols-4 gap-4">
29
+ <StatCard label="Adapters" value={adapters.length} />
30
+ <StatCard label="Endpoints" value={totalEndpoints} />
31
+ <StatCard label="Personas" value={dataSummary.length} />
32
+ <StatCard label="Total Entities" value={totalEntities} />
33
+ </div>
34
+
35
+ {/* Adapters grid */}
36
+ <div>
37
+ <h2 className="mb-4 text-lg font-semibold">API Adapters</h2>
38
+ <div className="grid grid-cols-3 gap-4">
39
+ {adapters.map((adapter) => (
40
+ <button
41
+ key={adapter.id}
42
+ onClick={() => onNavigate({ type: 'adapter', adapterId: adapter.id })}
43
+ className={cn(
44
+ 'rounded-lg border bg-card p-4 text-left transition-colors',
45
+ 'hover:border-primary/30 hover:bg-accent',
46
+ )}
47
+ >
48
+ <div className="flex items-center justify-between mb-2">
49
+ <span className="font-semibold">{adapter.name}</span>
50
+ <span
51
+ className={cn(
52
+ 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
53
+ adapter.enabled
54
+ ? 'bg-emerald-500/10 text-emerald-500'
55
+ : 'bg-zinc-500/10 text-zinc-500',
56
+ )}
57
+ >
58
+ {adapter.enabled ? 'active' : 'disabled'}
59
+ </span>
60
+ </div>
61
+ <p className="text-sm text-muted-foreground line-clamp-2 mb-3">
62
+ {adapter.description}
63
+ </p>
64
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
65
+ <span>{adapter.endpoints.length} endpoints</span>
66
+ {adapter.port && <span>:{adapter.port}</span>}
67
+ </div>
68
+ </button>
69
+ ))}
70
+ </div>
71
+ </div>
72
+
73
+ {/* Persona data overview */}
74
+ {dataSummary.length > 0 && (
75
+ <div>
76
+ <h2 className="mb-4 text-lg font-semibold">Persona Data</h2>
77
+ <div className="grid grid-cols-2 gap-4">
78
+ {dataSummary.map((ps) => (
79
+ <button
80
+ key={ps.persona}
81
+ onClick={() => onNavigate({ type: 'data', persona: ps.persona })}
82
+ className={cn(
83
+ 'rounded-lg border bg-card p-4 text-left transition-colors',
84
+ 'hover:border-primary/30 hover:bg-accent',
85
+ )}
86
+ >
87
+ <div className="font-semibold mb-3">{ps.persona}</div>
88
+
89
+ {Object.keys(ps.tables).length > 0 && (
90
+ <div className="mb-2">
91
+ <div className="text-xs font-medium text-muted-foreground mb-1">Database Tables</div>
92
+ <div className="flex flex-wrap gap-2">
93
+ {Object.entries(ps.tables).map(([table, count]) => (
94
+ <span key={table} className="inline-flex items-center gap-1 rounded bg-muted px-2 py-0.5 text-xs">
95
+ <span className="text-muted-foreground">{table}</span>
96
+ <span className="font-mono font-medium">{count}</span>
97
+ </span>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ {Object.keys(ps.apis).length > 0 && (
104
+ <div>
105
+ <div className="text-xs font-medium text-muted-foreground mb-1">API Resources</div>
106
+ <div className="flex flex-wrap gap-2">
107
+ {Object.entries(ps.apis).map(([adapterId, resources]) =>
108
+ Object.entries(resources).map(([resource, count]) => (
109
+ <span key={`${adapterId}-${resource}`} className="inline-flex items-center gap-1 rounded bg-muted px-2 py-0.5 text-xs">
110
+ <span className="text-muted-foreground">{adapterId}/{resource}</span>
111
+ <span className="font-mono font-medium">{count}</span>
112
+ </span>
113
+ )),
114
+ )}
115
+ </div>
116
+ </div>
117
+ )}
118
+ </button>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ function StatCard({ label, value }: { label: string; value: number }) {
128
+ return (
129
+ <div className="rounded-lg border bg-card p-4">
130
+ <div className="text-sm text-muted-foreground">{label}</div>
131
+ <div className="mt-1 text-3xl font-bold tracking-tight">{value}</div>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,202 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+ import type { PersonaDataSummary, DataResponse } from '@/lib/api';
4
+ import { JsonViewer } from '@/components/explorer/json-viewer';
5
+
6
+ interface DataViewProps {
7
+ persona: string;
8
+ source?: string;
9
+ resource?: string;
10
+ dataSummary: PersonaDataSummary[];
11
+ loadPersonaData: (persona: string) => Promise<DataResponse>;
12
+ }
13
+
14
+ export function DataView({ persona, dataSummary, loadPersonaData }: DataViewProps) {
15
+ const [data, setData] = useState<DataResponse | null>(null);
16
+ const [loading, setLoading] = useState(true);
17
+ const [selectedSource, setSelectedSource] = useState<string | null>(null);
18
+ const [selectedResource, setSelectedResource] = useState<string | null>(null);
19
+
20
+ useEffect(() => {
21
+ setLoading(true);
22
+ loadPersonaData(persona).then((d) => {
23
+ setData(d);
24
+ setLoading(false);
25
+ });
26
+ }, [persona]);
27
+
28
+ if (loading) {
29
+ return (
30
+ <div className="flex items-center justify-center h-64">
31
+ <span className="text-muted-foreground">Loading data...</span>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (!data) return null;
37
+
38
+ // Build navigation tree
39
+ const sources: Array<{
40
+ id: string;
41
+ label: string;
42
+ type: 'table' | 'api';
43
+ resources: Array<{ id: string; label: string; count: number }>;
44
+ }> = [];
45
+
46
+ // DB tables
47
+ if (Object.keys(data.tables).length > 0) {
48
+ sources.push({
49
+ id: 'database',
50
+ label: 'Database',
51
+ type: 'table',
52
+ resources: Object.entries(data.tables).map(([name, rows]) => ({
53
+ id: name,
54
+ label: name,
55
+ count: (rows as unknown[]).length,
56
+ })),
57
+ });
58
+ }
59
+
60
+ // API adapters
61
+ for (const [adapterId, responseSet] of Object.entries(data.apiResponses)) {
62
+ const resources = Object.entries(responseSet.responses)
63
+ .filter(([, arr]) => (arr as unknown[]).length > 0)
64
+ .map(([resource, arr]) => ({
65
+ id: resource,
66
+ label: resource,
67
+ count: (arr as unknown[]).length,
68
+ }));
69
+ if (resources.length > 0) {
70
+ sources.push({ id: adapterId, label: adapterId, type: 'api', resources });
71
+ }
72
+ }
73
+
74
+ const activeSource = selectedSource ?? sources[0]?.id ?? null;
75
+ const sourceObj = sources.find((s) => s.id === activeSource);
76
+ const activeResource = selectedResource ?? sourceObj?.resources[0]?.id ?? null;
77
+
78
+ const getActiveData = (): unknown[] => {
79
+ if (!sourceObj || !activeResource) return [];
80
+ if (sourceObj.type === 'table') {
81
+ return (data.tables[activeResource] as unknown[]) ?? [];
82
+ }
83
+ return (data.apiResponses[activeSource!]?.responses[activeResource] as unknown[]) ?? [];
84
+ };
85
+
86
+ const activeData = getActiveData();
87
+
88
+ return (
89
+ <div className="space-y-6">
90
+ <div>
91
+ <h1 className="text-2xl font-bold">{persona}</h1>
92
+ <p className="text-muted-foreground mt-1">
93
+ Generated data for this persona across all configured surfaces.
94
+ </p>
95
+ </div>
96
+
97
+ {/* Facts */}
98
+ {data.facts && data.facts.length > 0 && (
99
+ <div>
100
+ <h2 className="mb-3 text-lg font-semibold">Facts ({data.facts.length})</h2>
101
+ <div className="grid grid-cols-2 gap-3">
102
+ {data.facts.map((fact) => (
103
+ <div
104
+ key={fact.id}
105
+ className={cn(
106
+ 'rounded-lg border p-3',
107
+ fact.severity === 'critical' && 'border-red-500/30 bg-red-500/5',
108
+ fact.severity === 'warn' && 'border-amber-500/30 bg-amber-500/5',
109
+ fact.severity === 'info' && 'border-blue-500/30 bg-blue-500/5',
110
+ )}
111
+ >
112
+ <div className="flex items-center gap-2 mb-1">
113
+ <span
114
+ className={cn(
115
+ 'text-xs font-medium rounded px-1.5 py-0.5',
116
+ fact.severity === 'critical' && 'bg-red-500/10 text-red-500',
117
+ fact.severity === 'warn' && 'bg-amber-500/10 text-amber-500',
118
+ fact.severity === 'info' && 'bg-blue-500/10 text-blue-500',
119
+ )}
120
+ >
121
+ {fact.severity}
122
+ </span>
123
+ <span className="text-xs text-muted-foreground">{fact.platform}</span>
124
+ <span className="text-xs text-muted-foreground">{fact.type}</span>
125
+ </div>
126
+ <p className="text-sm">{fact.detail}</p>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ </div>
131
+ )}
132
+
133
+ {/* Source / Resource navigation + data viewer */}
134
+ <div className="flex gap-4 min-h-[500px]">
135
+ {/* Source tree */}
136
+ <div className="w-56 shrink-0 space-y-2">
137
+ {sources.map((source) => (
138
+ <div key={source.id}>
139
+ <div
140
+ className={cn(
141
+ 'px-2 py-1 text-xs font-semibold uppercase tracking-wider cursor-pointer rounded',
142
+ activeSource === source.id
143
+ ? 'text-foreground'
144
+ : 'text-muted-foreground hover:text-foreground',
145
+ )}
146
+ onClick={() => {
147
+ setSelectedSource(source.id);
148
+ setSelectedResource(source.resources[0]?.id ?? null);
149
+ }}
150
+ >
151
+ {source.label}
152
+ <span className="ml-1.5 font-normal normal-case">
153
+ ({source.type === 'api' ? 'api' : 'db'})
154
+ </span>
155
+ </div>
156
+ {activeSource === source.id && (
157
+ <div className="ml-2 mt-1 space-y-0.5">
158
+ {source.resources.map((res) => (
159
+ <button
160
+ key={res.id}
161
+ onClick={() => setSelectedResource(res.id)}
162
+ className={cn(
163
+ 'flex w-full items-center justify-between rounded px-2 py-1 text-sm transition-colors',
164
+ activeResource === res.id
165
+ ? 'bg-accent text-accent-foreground'
166
+ : 'text-muted-foreground hover:bg-accent/50',
167
+ )}
168
+ >
169
+ <span className="truncate">{res.label}</span>
170
+ <span className="text-xs font-mono">{res.count}</span>
171
+ </button>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+ ))}
177
+ </div>
178
+
179
+ {/* Data panel */}
180
+ <div className="flex-1 rounded-lg border bg-card overflow-hidden">
181
+ <div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2">
182
+ <span className="text-sm font-medium">
183
+ {activeResource ?? 'Select a resource'}
184
+ </span>
185
+ <span className="text-xs text-muted-foreground">
186
+ {activeData.length} {activeData.length === 1 ? 'record' : 'records'}
187
+ </span>
188
+ </div>
189
+ <div className="overflow-auto max-h-[600px] p-4">
190
+ {activeData.length > 0 ? (
191
+ <JsonViewer data={activeData} />
192
+ ) : (
193
+ <div className="text-center text-muted-foreground py-12">
194
+ No data available
195
+ </div>
196
+ )}
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ );
202
+ }