@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.
package/src/App.tsx ADDED
@@ -0,0 +1,116 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { api, type ExplorerConfig, type AdapterInfo, type PersonaDataSummary, type DataResponse } from './lib/api';
3
+ import { Sidebar } from './components/layout/sidebar';
4
+ import { Header } from './components/layout/header';
5
+ import { Dashboard } from './pages/dashboard';
6
+ import { AdapterView } from './pages/adapter-view';
7
+ import { DataView } from './pages/data-view';
8
+ import { EndpointTester } from './pages/endpoint-tester';
9
+
10
+ export type Page =
11
+ | { type: 'dashboard' }
12
+ | { type: 'adapter'; adapterId: string }
13
+ | { type: 'data'; persona: string; source?: string; resource?: string }
14
+ | { type: 'tester'; adapterId: string; endpoint?: { method: string; path: string } };
15
+
16
+ export function App() {
17
+ const [config, setConfig] = useState<ExplorerConfig | null>(null);
18
+ const [adapters, setAdapters] = useState<AdapterInfo[]>([]);
19
+ const [dataSummary, setDataSummary] = useState<PersonaDataSummary[]>([]);
20
+ const [personaData, setPersonaData] = useState<Record<string, DataResponse>>({});
21
+ const [page, setPage] = useState<Page>({ type: 'dashboard' });
22
+ const [loading, setLoading] = useState(true);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ useEffect(() => {
26
+ Promise.all([api.getConfig(), api.getAdapters(), api.getDataSummary()])
27
+ .then(([cfg, adp, data]) => {
28
+ setConfig(cfg);
29
+ setAdapters(adp);
30
+ setDataSummary(data);
31
+ setLoading(false);
32
+ })
33
+ .catch((err) => {
34
+ setError(err.message);
35
+ setLoading(false);
36
+ });
37
+ }, []);
38
+
39
+ const loadPersonaData = async (persona: string) => {
40
+ if (personaData[persona]) return personaData[persona];
41
+ const data = await api.getPersonaData(persona);
42
+ setPersonaData((prev) => ({ ...prev, [persona]: data }));
43
+ return data;
44
+ };
45
+
46
+ if (loading) {
47
+ return (
48
+ <div className="flex h-screen items-center justify-center">
49
+ <div className="text-center">
50
+ <div className="mb-4 text-4xl font-bold tracking-tight">mimic</div>
51
+ <div className="text-muted-foreground">Loading explorer...</div>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ if (error) {
58
+ return (
59
+ <div className="flex h-screen items-center justify-center">
60
+ <div className="text-center max-w-md">
61
+ <div className="mb-4 text-4xl font-bold tracking-tight text-destructive">Error</div>
62
+ <div className="text-muted-foreground mb-4">{error}</div>
63
+ <div className="text-sm text-muted-foreground">
64
+ Make sure <code className="bg-muted px-1 py-0.5 rounded">mimic host</code> is running first.
65
+ </div>
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <div className="flex h-screen overflow-hidden">
73
+ <Sidebar
74
+ config={config!}
75
+ adapters={adapters}
76
+ dataSummary={dataSummary}
77
+ page={page}
78
+ onNavigate={setPage}
79
+ />
80
+ <div className="flex flex-1 flex-col overflow-hidden">
81
+ <Header config={config!} page={page} onNavigate={setPage} />
82
+ <main className="flex-1 overflow-auto p-6">
83
+ {page.type === 'dashboard' && (
84
+ <Dashboard
85
+ config={config!}
86
+ adapters={adapters}
87
+ dataSummary={dataSummary}
88
+ onNavigate={setPage}
89
+ />
90
+ )}
91
+ {page.type === 'adapter' && (
92
+ <AdapterView
93
+ adapter={adapters.find((a) => a.id === page.adapterId)!}
94
+ onNavigate={setPage}
95
+ />
96
+ )}
97
+ {page.type === 'data' && (
98
+ <DataView
99
+ persona={page.persona}
100
+ source={page.source}
101
+ resource={page.resource}
102
+ dataSummary={dataSummary}
103
+ loadPersonaData={loadPersonaData}
104
+ />
105
+ )}
106
+ {page.type === 'tester' && (
107
+ <EndpointTester
108
+ adapter={adapters.find((a) => a.id === page.adapterId)!}
109
+ initialEndpoint={page.endpoint}
110
+ />
111
+ )}
112
+ </main>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,241 @@
1
+ import { useState } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ interface JsonViewerProps {
5
+ data: unknown;
6
+ initialExpanded?: boolean;
7
+ }
8
+
9
+ export function JsonViewer({ data, initialExpanded = true }: JsonViewerProps) {
10
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object' && data[0] !== null) {
11
+ return <TableView data={data as Record<string, unknown>[]}/>;
12
+ }
13
+ return (
14
+ <pre className="text-sm font-mono leading-relaxed">
15
+ <JsonNode value={data} depth={0} initialExpanded={initialExpanded} />
16
+ </pre>
17
+ );
18
+ }
19
+
20
+ function TableView({ data }: { data: Record<string, unknown>[] }) {
21
+ const [expanded, setExpanded] = useState<number | null>(null);
22
+
23
+ // Get column keys from first few items
24
+ const allKeys = new Set<string>();
25
+ data.slice(0, 10).forEach((item) => Object.keys(item).forEach((k) => allKeys.add(k)));
26
+ const columns = Array.from(allKeys).slice(0, 8);
27
+ const hasMore = allKeys.size > 8;
28
+
29
+ return (
30
+ <div className="space-y-2">
31
+ <div className="overflow-x-auto">
32
+ <table className="w-full text-sm">
33
+ <thead>
34
+ <tr className="border-b">
35
+ <th className="px-2 py-1.5 text-left text-xs font-medium text-muted-foreground w-8">#</th>
36
+ {columns.map((col) => (
37
+ <th key={col} className="px-2 py-1.5 text-left text-xs font-medium text-muted-foreground">
38
+ {col}
39
+ </th>
40
+ ))}
41
+ {hasMore && (
42
+ <th className="px-2 py-1.5 text-left text-xs font-medium text-muted-foreground">...</th>
43
+ )}
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ {data.map((row, i) => (
48
+ <>
49
+ <tr
50
+ key={i}
51
+ onClick={() => setExpanded(expanded === i ? null : i)}
52
+ className={cn(
53
+ 'cursor-pointer transition-colors border-b border-border/50',
54
+ 'hover:bg-muted/30',
55
+ expanded === i && 'bg-muted/50',
56
+ )}
57
+ >
58
+ <td className="px-2 py-1.5 text-xs text-muted-foreground">{i + 1}</td>
59
+ {columns.map((col) => (
60
+ <td key={col} className="px-2 py-1.5 font-mono text-xs max-w-[200px] truncate">
61
+ <CellValue value={row[col]} />
62
+ </td>
63
+ ))}
64
+ {hasMore && <td className="px-2 py-1.5 text-xs text-muted-foreground">...</td>}
65
+ </tr>
66
+ {expanded === i && (
67
+ <tr key={`${i}-expanded`}>
68
+ <td colSpan={columns.length + (hasMore ? 2 : 1)} className="p-3 bg-muted/20">
69
+ <pre className="text-xs font-mono whitespace-pre-wrap">
70
+ <JsonNode value={row} depth={0} initialExpanded={true} />
71
+ </pre>
72
+ </td>
73
+ </tr>
74
+ )}
75
+ </>
76
+ ))}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ {data.length > 100 && (
81
+ <div className="text-xs text-muted-foreground text-center py-2">
82
+ Showing all {data.length} records
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Full pretty-printed JSON with syntax highlighting — used for API responses.
91
+ */
92
+ export function RawJsonViewer({ data }: { data: unknown }) {
93
+ const json = JSON.stringify(data, null, 2) ?? '';
94
+ return (
95
+ <pre className="text-sm font-mono leading-relaxed whitespace-pre-wrap break-all">
96
+ {json.split('\n').map((line, i) => (
97
+ <span key={i}>
98
+ {highlightJsonLine(line)}
99
+ {'\n'}
100
+ </span>
101
+ ))}
102
+ </pre>
103
+ );
104
+ }
105
+
106
+ function highlightJsonLine(line: string): React.ReactNode {
107
+ // Match key-value patterns in JSON
108
+ const kvMatch = line.match(/^(\s*)"([^"]+)":\s*(.*)/);
109
+ if (kvMatch) {
110
+ const [, indent, key, rest] = kvMatch;
111
+ return (
112
+ <>
113
+ {indent}<span className="json-key">"{key}"</span>: {highlightValue(rest)}
114
+ </>
115
+ );
116
+ }
117
+ // Standalone value (array element)
118
+ const valMatch = line.match(/^(\s*)(.*)/);
119
+ if (valMatch) {
120
+ const [, indent, val] = valMatch;
121
+ return <>{indent}{highlightValue(val)}</>;
122
+ }
123
+ return line;
124
+ }
125
+
126
+ function highlightValue(val: string): React.ReactNode {
127
+ const trimmed = val.replace(/,\s*$/, '');
128
+ const comma = val.endsWith(',') ? ',' : '';
129
+
130
+ if (trimmed === 'null') return <><span className="json-null">null</span>{comma}</>;
131
+ if (trimmed === 'true' || trimmed === 'false') return <><span className="json-boolean">{trimmed}</span>{comma}</>;
132
+ if (/^-?\d/.test(trimmed)) return <><span className="json-number">{trimmed}</span>{comma}</>;
133
+ if (trimmed.startsWith('"')) return <><span className="json-string">{trimmed}</span>{comma}</>;
134
+ return <>{val}</>;
135
+ }
136
+
137
+ function CellValue({ value }: { value: unknown }) {
138
+ if (value === null || value === undefined) {
139
+ return <span className="json-null">null</span>;
140
+ }
141
+ if (typeof value === 'boolean') {
142
+ return <span className="json-boolean">{String(value)}</span>;
143
+ }
144
+ if (typeof value === 'number') {
145
+ return <span className="json-number">{value}</span>;
146
+ }
147
+ if (typeof value === 'string') {
148
+ if (value.length > 40) return <span className="json-string" title={value}>{value.slice(0, 37)}...</span>;
149
+ return <span className="json-string">{value}</span>;
150
+ }
151
+ if (typeof value === 'object') {
152
+ return <span className="text-muted-foreground">{Array.isArray(value) ? `[${value.length}]` : '{...}'}</span>;
153
+ }
154
+ return <span>{String(value)}</span>;
155
+ }
156
+
157
+ function JsonNode({
158
+ value,
159
+ depth,
160
+ initialExpanded,
161
+ }: {
162
+ value: unknown;
163
+ depth: number;
164
+ initialExpanded: boolean;
165
+ }) {
166
+ const [isExpanded, setIsExpanded] = useState(initialExpanded && depth < 4);
167
+
168
+ if (value === null) return <span className="json-null">null</span>;
169
+ if (value === undefined) return <span className="json-null">undefined</span>;
170
+ if (typeof value === 'boolean') return <span className="json-boolean">{String(value)}</span>;
171
+ if (typeof value === 'number') return <span className="json-number">{value}</span>;
172
+ if (typeof value === 'string') return <span className="json-string">"{value}"</span>;
173
+
174
+ if (Array.isArray(value)) {
175
+ if (value.length === 0) return <span>{'[]'}</span>;
176
+ const indent = ' '.repeat(depth);
177
+ const childIndent = ' '.repeat(depth + 1);
178
+
179
+ return (
180
+ <span>
181
+ <span
182
+ className="cursor-pointer hover:text-primary"
183
+ onClick={() => setIsExpanded(!isExpanded)}
184
+ >
185
+ {isExpanded ? '[' : `[${value.length} items]`}
186
+ </span>
187
+ {isExpanded && (
188
+ <>
189
+ {'\n'}
190
+ {value.map((item, i) => (
191
+ <span key={i}>
192
+ {childIndent}
193
+ <JsonNode value={item} depth={depth + 1} initialExpanded={initialExpanded} />
194
+ {i < value.length - 1 ? ',' : ''}
195
+ {'\n'}
196
+ </span>
197
+ ))}
198
+ {indent}]
199
+ </>
200
+ )}
201
+ </span>
202
+ );
203
+ }
204
+
205
+ if (typeof value === 'object') {
206
+ const entries = Object.entries(value as Record<string, unknown>);
207
+ if (entries.length === 0) return <span>{'{}'}</span>;
208
+ const indent = ' '.repeat(depth);
209
+ const childIndent = ' '.repeat(depth + 1);
210
+
211
+ return (
212
+ <span>
213
+ <span
214
+ className="cursor-pointer hover:text-primary"
215
+ onClick={() => setIsExpanded(!isExpanded)}
216
+ >
217
+ {isExpanded ? '{' : `{${entries.length} keys}`}
218
+ </span>
219
+ {isExpanded && (
220
+ <>
221
+ {'\n'}
222
+ {entries.map(([key, val], i) => (
223
+ <span key={key}>
224
+ {childIndent}
225
+ <span className="json-key">"{key}"</span>
226
+ {': '}
227
+ <JsonNode value={val} depth={depth + 1} initialExpanded={initialExpanded} />
228
+ {i < entries.length - 1 ? ',' : ''}
229
+ {'\n'}
230
+ </span>
231
+ ))}
232
+ {indent}
233
+ {'}'}
234
+ </>
235
+ )}
236
+ </span>
237
+ );
238
+ }
239
+
240
+ return <span>{String(value)}</span>;
241
+ }
@@ -0,0 +1,57 @@
1
+ import type { ExplorerConfig } from '@/lib/api';
2
+ import type { Page } from '@/App';
3
+
4
+ interface HeaderProps {
5
+ config: ExplorerConfig;
6
+ page: Page;
7
+ onNavigate: (page: Page) => void;
8
+ }
9
+
10
+ export function Header({ config, page, onNavigate }: HeaderProps) {
11
+ const breadcrumbs = getBreadcrumbs(page);
12
+
13
+ return (
14
+ <header className="flex h-14 items-center justify-between border-b px-6">
15
+ <div className="flex items-center gap-2 text-sm">
16
+ {breadcrumbs.map((crumb, i) => (
17
+ <span key={i} className="flex items-center gap-2">
18
+ {i > 0 && <span className="text-muted-foreground">/</span>}
19
+ {crumb.onClick ? (
20
+ <button
21
+ onClick={crumb.onClick}
22
+ className="text-muted-foreground hover:text-foreground transition-colors"
23
+ >
24
+ {crumb.label}
25
+ </button>
26
+ ) : (
27
+ <span className="font-medium">{crumb.label}</span>
28
+ )}
29
+ </span>
30
+ ))}
31
+ </div>
32
+ <div className="flex items-center gap-3">
33
+ <span className="text-xs font-mono text-muted-foreground">
34
+ {config.domain}
35
+ </span>
36
+ </div>
37
+ </header>
38
+ );
39
+ }
40
+
41
+ function getBreadcrumbs(page: Page): Array<{ label: string; onClick?: () => void }> {
42
+ switch (page.type) {
43
+ case 'dashboard':
44
+ return [{ label: 'Dashboard' }];
45
+ case 'adapter':
46
+ return [{ label: 'Adapters' }, { label: page.adapterId }];
47
+ case 'data':
48
+ return [
49
+ { label: 'Data' },
50
+ { label: page.persona },
51
+ ...(page.source ? [{ label: page.source }] : []),
52
+ ...(page.resource ? [{ label: page.resource }] : []),
53
+ ];
54
+ case 'tester':
55
+ return [{ label: 'Adapters' }, { label: page.adapterId }, { label: 'Test' }];
56
+ }
57
+ }
@@ -0,0 +1,132 @@
1
+ import { cn } from '@/lib/utils';
2
+ import type { ExplorerConfig, AdapterInfo, PersonaDataSummary } from '@/lib/api';
3
+ import type { Page } from '@/App';
4
+
5
+ interface SidebarProps {
6
+ config: ExplorerConfig;
7
+ adapters: AdapterInfo[];
8
+ dataSummary: PersonaDataSummary[];
9
+ page: Page;
10
+ onNavigate: (page: Page) => void;
11
+ }
12
+
13
+ export function Sidebar({ config, adapters, dataSummary, page, onNavigate }: SidebarProps) {
14
+ return (
15
+ <aside className="flex w-64 flex-col border-r bg-card">
16
+ {/* Logo */}
17
+ <div
18
+ className="flex h-14 items-center gap-2 border-b px-4 cursor-pointer"
19
+ onClick={() => onNavigate({ type: 'dashboard' })}
20
+ >
21
+ <span className="text-xl font-bold tracking-tight">mimic</span>
22
+ <span className="text-xs text-muted-foreground font-mono">explorer</span>
23
+ </div>
24
+
25
+ <nav className="flex-1 overflow-auto p-3 space-y-6">
26
+ {/* Adapters */}
27
+ <div>
28
+ <div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
29
+ API Adapters
30
+ </div>
31
+ <div className="space-y-0.5">
32
+ {adapters.map((adapter) => (
33
+ <button
34
+ key={adapter.id}
35
+ onClick={() => onNavigate({ type: 'adapter', adapterId: adapter.id })}
36
+ className={cn(
37
+ 'flex w-full items-center justify-between rounded-md px-2 py-1.5 text-sm transition-colors',
38
+ 'hover:bg-accent hover:text-accent-foreground',
39
+ page.type === 'adapter' && page.adapterId === adapter.id
40
+ ? 'bg-accent text-accent-foreground'
41
+ : 'text-muted-foreground',
42
+ )}
43
+ >
44
+ <span className="truncate">{adapter.name}</span>
45
+ <span className="flex items-center gap-1.5">
46
+ <span className="text-xs text-muted-foreground">{adapter.endpoints.length}</span>
47
+ <span
48
+ className={cn(
49
+ 'h-2 w-2 rounded-full',
50
+ adapter.enabled ? 'bg-emerald-500' : 'bg-zinc-500',
51
+ )}
52
+ />
53
+ </span>
54
+ </button>
55
+ ))}
56
+ {adapters.length === 0 && (
57
+ <div className="px-2 py-3 text-xs text-muted-foreground">
58
+ No adapters configured
59
+ </div>
60
+ )}
61
+ </div>
62
+ </div>
63
+
64
+ {/* Persona Data */}
65
+ <div>
66
+ <div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
67
+ Persona Data
68
+ </div>
69
+ <div className="space-y-0.5">
70
+ {dataSummary.map((ps) => {
71
+ const totalEntities = Object.values(ps.tables).reduce((s, n) => s + n, 0)
72
+ + Object.values(ps.apis).reduce(
73
+ (s, resources) => s + Object.values(resources).reduce((s2, n) => s2 + n, 0),
74
+ 0,
75
+ );
76
+
77
+ return (
78
+ <button
79
+ key={ps.persona}
80
+ onClick={() => onNavigate({ type: 'data', persona: ps.persona })}
81
+ className={cn(
82
+ 'flex w-full items-center justify-between rounded-md px-2 py-1.5 text-sm transition-colors',
83
+ 'hover:bg-accent hover:text-accent-foreground',
84
+ page.type === 'data' && page.persona === ps.persona
85
+ ? 'bg-accent text-accent-foreground'
86
+ : 'text-muted-foreground',
87
+ )}
88
+ >
89
+ <span className="truncate">{ps.persona}</span>
90
+ <span className="text-xs text-muted-foreground">{totalEntities}</span>
91
+ </button>
92
+ );
93
+ })}
94
+ {dataSummary.length === 0 && (
95
+ <div className="px-2 py-3 text-xs text-muted-foreground">
96
+ No data generated yet. Run <code className="bg-muted px-1 rounded">mimic run</code>
97
+ </div>
98
+ )}
99
+ </div>
100
+ </div>
101
+
102
+ {/* Databases */}
103
+ {config.databases && Object.keys(config.databases).length > 0 && (
104
+ <div>
105
+ <div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
106
+ Databases
107
+ </div>
108
+ <div className="space-y-0.5">
109
+ {Object.entries(config.databases).map(([name, db]) => (
110
+ <div
111
+ key={name}
112
+ className="flex items-center justify-between rounded-md px-2 py-1.5 text-sm text-muted-foreground"
113
+ >
114
+ <span className="truncate">{name}</span>
115
+ <span className="text-xs font-mono">{db.type}</span>
116
+ </div>
117
+ ))}
118
+ </div>
119
+ </div>
120
+ )}
121
+ </nav>
122
+
123
+ {/* Footer */}
124
+ <div className="border-t p-3">
125
+ <div className="text-xs text-muted-foreground">
126
+ <div>{config.llm.provider} / {config.llm.model}</div>
127
+ <div className="mt-0.5">{config.domain}</div>
128
+ </div>
129
+ </div>
130
+ </aside>
131
+ );
132
+ }
package/src/index.css ADDED
@@ -0,0 +1,69 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 240 10% 3.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 240 10% 3.9%;
11
+ --primary: 240 5.9% 10%;
12
+ --primary-foreground: 0 0% 98%;
13
+ --secondary: 240 4.8% 95.9%;
14
+ --secondary-foreground: 240 5.9% 10%;
15
+ --muted: 240 4.8% 95.9%;
16
+ --muted-foreground: 240 3.8% 46.1%;
17
+ --accent: 240 4.8% 95.9%;
18
+ --accent-foreground: 240 5.9% 10%;
19
+ --destructive: 0 84.2% 60.2%;
20
+ --destructive-foreground: 0 0% 98%;
21
+ --border: 240 5.9% 90%;
22
+ --input: 240 5.9% 90%;
23
+ --ring: 240 5.9% 10%;
24
+ --radius: 0.5rem;
25
+ }
26
+
27
+ .dark {
28
+ --background: 240 10% 3.9%;
29
+ --foreground: 0 0% 98%;
30
+ --card: 240 10% 3.9%;
31
+ --card-foreground: 0 0% 98%;
32
+ --primary: 0 0% 98%;
33
+ --primary-foreground: 240 5.9% 10%;
34
+ --secondary: 240 3.7% 15.9%;
35
+ --secondary-foreground: 0 0% 98%;
36
+ --muted: 240 3.7% 15.9%;
37
+ --muted-foreground: 240 5% 64.9%;
38
+ --accent: 240 3.7% 15.9%;
39
+ --accent-foreground: 0 0% 98%;
40
+ --destructive: 0 62.8% 30.6%;
41
+ --destructive-foreground: 0 0% 98%;
42
+ --border: 240 3.7% 15.9%;
43
+ --input: 240 3.7% 15.9%;
44
+ --ring: 240 4.9% 83.9%;
45
+ --radius: 0.5rem;
46
+ }
47
+ }
48
+
49
+ @layer base {
50
+ * {
51
+ @apply border-border;
52
+ }
53
+ body {
54
+ @apply bg-background text-foreground;
55
+ }
56
+ }
57
+
58
+ /* JSON syntax highlighting */
59
+ .json-key { color: hsl(210 80% 65%); }
60
+ .json-string { color: hsl(120 50% 60%); }
61
+ .json-number { color: hsl(35 90% 65%); }
62
+ .json-boolean { color: hsl(280 60% 70%); }
63
+ .json-null { color: hsl(0 0% 50%); }
64
+
65
+ /* Scrollbar styling */
66
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
67
+ ::-webkit-scrollbar-track { background: transparent; }
68
+ ::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 3px; }
69
+ ::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
package/src/lib/api.ts ADDED
@@ -0,0 +1,57 @@
1
+ const BASE = '/_api';
2
+
3
+ export interface ExplorerConfig {
4
+ domain: string;
5
+ personas: Array<{ name: string; description: string; blueprint?: string }>;
6
+ llm: { provider: string; model: string };
7
+ apis: Record<string, { adapter?: string; enabled?: boolean; port?: number; mcp?: boolean }>;
8
+ databases: Record<string, { type: string; url?: string }>;
9
+ }
10
+
11
+ export interface AdapterInfo {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ type: string;
16
+ basePath: string;
17
+ versions: string[];
18
+ endpoints: Array<{ method: string; path: string; description: string }>;
19
+ enabled: boolean;
20
+ port?: number;
21
+ }
22
+
23
+ export interface PersonaDataSummary {
24
+ persona: string;
25
+ tables: Record<string, number>;
26
+ apis: Record<string, Record<string, number>>;
27
+ }
28
+
29
+ export interface DataResponse {
30
+ persona: string;
31
+ tables: Record<string, unknown[]>;
32
+ apiResponses: Record<string, { responses: Record<string, unknown[]> }>;
33
+ facts: Array<{ id: string; type: string; platform: string; severity: string; detail: string }>;
34
+ }
35
+
36
+ async function fetchJson<T>(path: string): Promise<T> {
37
+ const res = await fetch(`${BASE}${path}`);
38
+ if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
39
+ return res.json();
40
+ }
41
+
42
+ export const api = {
43
+ getConfig: () => fetchJson<ExplorerConfig>('/config'),
44
+ getAdapters: () => fetchJson<AdapterInfo[]>('/adapters'),
45
+ getDataSummary: () => fetchJson<PersonaDataSummary[]>('/data'),
46
+ getPersonaData: (persona: string) => fetchJson<DataResponse>(`/data/${persona}`),
47
+ testEndpoint: (port: number, method: string, path: string, body?: unknown) =>
48
+ fetch(`http://localhost:${port}${path}`, {
49
+ method,
50
+ headers: { 'Content-Type': 'application/json' },
51
+ ...(body ? { body: JSON.stringify(body) } : {}),
52
+ }).then(async (res) => ({
53
+ status: res.status,
54
+ headers: Object.fromEntries(res.headers.entries()),
55
+ body: await res.json().catch(() => res.text()),
56
+ })),
57
+ };