@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/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +190 -0
- package/dist/client/assets/index-6jl2Igpx.css +1 -0
- package/dist/client/assets/index-DrcjTHum.js +55 -0
- package/dist/client/index.html +14 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.js +210 -0
- package/index.html +13 -0
- package/package.json +45 -0
- package/postcss.config.js +6 -0
- package/server/index.ts +285 -0
- package/src/App.tsx +116 -0
- package/src/components/explorer/json-viewer.tsx +241 -0
- package/src/components/layout/header.tsx +57 -0
- package/src/components/layout/sidebar.tsx +132 -0
- package/src/index.css +69 -0
- package/src/lib/api.ts +57 -0
- package/src/lib/group-endpoints.ts +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/adapter-view.tsx +128 -0
- package/src/pages/dashboard.tsx +134 -0
- package/src/pages/data-view.tsx +202 -0
- package/src/pages/endpoint-tester.tsx +239 -0
- package/tailwind.config.ts +49 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +11 -0
- package/vite.config.ts +22 -0
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
|
+
};
|