@anchrd/intel-ui 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/README.md +18 -0
- package/components.json +15 -0
- package/index.html +13 -0
- package/package.json +63 -0
- package/src/app/app.tsx +62 -0
- package/src/blocknote-view/blocknote-view.tsx +13 -0
- package/src/branding/branding.tsx +17 -0
- package/src/branding/custom-logo.ts +1 -0
- package/src/branding/favicon.default.svg +5 -0
- package/src/branding/favicon.svg +5 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +338 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +85 -0
- package/src/flows/flows.tsx +921 -0
- package/src/i18n/custom.json +4 -0
- package/src/i18n/en.json +124 -0
- package/src/i18n/i18n.ts +28 -0
- package/src/i18n/i18n.types.ts +6 -0
- package/src/knowledge/knowledge.tsx +825 -0
- package/src/knowledge-editor/knowledge-editor.tsx +92 -0
- package/src/knowledge-graph/knowledge-graph.ts +58 -0
- package/src/knowledge-graph/knowledge-graph.tsx +111 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +26 -0
- package/src/modal/modal.tsx +53 -0
- package/src/router/router-context.ts +6 -0
- package/src/router/router.tsx +42 -0
- package/src/router/router.types.ts +7 -0
- package/src/styles.css +120 -0
- package/src/theme/custom.css +1 -0
- package/src/theme/theme.ts +16 -0
- package/src/tools/tools.tsx +346 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { ToolCatalogEntry } from "@anchrd/intel-contract";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
CheckCircle2,
|
|
6
|
+
CircleOff,
|
|
7
|
+
KeyRound,
|
|
8
|
+
LogIn,
|
|
9
|
+
Play,
|
|
10
|
+
Plus,
|
|
11
|
+
RefreshCw,
|
|
12
|
+
ShieldAlert,
|
|
13
|
+
Wrench,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { useMemo, useState } from "react";
|
|
16
|
+
import { Modal } from "@/modal/modal.tsx";
|
|
17
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
18
|
+
|
|
19
|
+
const statusIcon = {
|
|
20
|
+
connected: CheckCircle2,
|
|
21
|
+
missing: KeyRound,
|
|
22
|
+
forbidden: ShieldAlert,
|
|
23
|
+
invalid: CircleOff,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export function Tools() {
|
|
27
|
+
const { data, i18n } = useIntelRouterContext();
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
const sources = useQuery({ queryKey: ["tool-sources"], queryFn: () => data.listToolSources() });
|
|
30
|
+
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
31
|
+
const [registering, setRegistering] = useState(false);
|
|
32
|
+
const [selected, setSelected] = useState<ToolCatalogEntry | null>(null);
|
|
33
|
+
const connectError =
|
|
34
|
+
typeof window === "undefined"
|
|
35
|
+
? null
|
|
36
|
+
: new URLSearchParams(window.location.search).get("connectError");
|
|
37
|
+
const grouped = useMemo(() => {
|
|
38
|
+
const entries = catalog.data?.items ?? [];
|
|
39
|
+
return (sources.data?.items ?? []).map((source) => ({
|
|
40
|
+
source,
|
|
41
|
+
entries: entries.filter((entry) => entry.source.id === source.id),
|
|
42
|
+
}));
|
|
43
|
+
}, [catalog.data, sources.data]);
|
|
44
|
+
const discover = useMutation({
|
|
45
|
+
mutationFn: (sourceId: string) =>
|
|
46
|
+
data.discoverToolSource({ sourceId, idempotencyKey: crypto.randomUUID() }),
|
|
47
|
+
onSuccess: async () => {
|
|
48
|
+
await Promise.all([
|
|
49
|
+
queryClient.invalidateQueries({ queryKey: ["tool-sources"] }),
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ["tools"] }),
|
|
51
|
+
]);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="min-h-screen">
|
|
57
|
+
<header className="flex items-center justify-between gap-6 border-b px-8 py-5">
|
|
58
|
+
<div>
|
|
59
|
+
<h1 className="text-xl font-semibold tracking-tight">{i18n.t("tools.title")}</h1>
|
|
60
|
+
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.description")}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setRegistering(true)}
|
|
65
|
+
className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring"
|
|
66
|
+
>
|
|
67
|
+
<Plus aria-hidden="true" className="size-4" />
|
|
68
|
+
{i18n.t("tools.addSource")}
|
|
69
|
+
</button>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<div className="grid gap-5 p-8 xl:grid-cols-[minmax(0,1fr)_24rem]">
|
|
73
|
+
<div className="space-y-5">
|
|
74
|
+
{discover.isError && (
|
|
75
|
+
<p
|
|
76
|
+
role="alert"
|
|
77
|
+
className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
|
|
78
|
+
>
|
|
79
|
+
{i18n.t("tools.operationFailed")}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
{connectError && (
|
|
83
|
+
<p
|
|
84
|
+
role="alert"
|
|
85
|
+
className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
|
|
86
|
+
>
|
|
87
|
+
{i18n.t("tools.connectFailed")}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
{sources.isPending || catalog.isPending ? (
|
|
91
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
92
|
+
) : grouped.length === 0 ? (
|
|
93
|
+
<section className="rounded-xl border border-dashed bg-card p-12 text-center">
|
|
94
|
+
<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />
|
|
95
|
+
<h2 className="mt-4 font-semibold">{i18n.t("tools.empty")}</h2>
|
|
96
|
+
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">
|
|
97
|
+
{i18n.t("tools.emptyHelp")}
|
|
98
|
+
</p>
|
|
99
|
+
</section>
|
|
100
|
+
) : (
|
|
101
|
+
grouped.map(({ source, entries }) => (
|
|
102
|
+
<section
|
|
103
|
+
key={source.id}
|
|
104
|
+
className="overflow-hidden rounded-xl border bg-card shadow-sm"
|
|
105
|
+
>
|
|
106
|
+
<div className="flex items-start justify-between gap-5 border-b bg-muted/20 px-5 py-4">
|
|
107
|
+
<div className="min-w-0">
|
|
108
|
+
<h2 className="font-semibold">{source.name}</h2>
|
|
109
|
+
<p className="mt-1 truncate text-xs text-muted-foreground">{source.url}</p>
|
|
110
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
111
|
+
{i18n.t("tools.connectionHandle")}: {source.connectionHandle}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
115
|
+
<a
|
|
116
|
+
href={data.toolSourceConnectUrl(source.id, "/tools")}
|
|
117
|
+
className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
118
|
+
>
|
|
119
|
+
<LogIn aria-hidden="true" className="size-3.5" />
|
|
120
|
+
{i18n.t("tools.connect")}
|
|
121
|
+
</a>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
disabled={discover.isPending && discover.variables === source.id}
|
|
125
|
+
onClick={() => discover.mutate(source.id)}
|
|
126
|
+
className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
127
|
+
>
|
|
128
|
+
<RefreshCw aria-hidden="true" className="size-3.5" />
|
|
129
|
+
{i18n.t("tools.discover")}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{entries.length === 0 ? (
|
|
134
|
+
<p className="p-5 text-sm text-muted-foreground">
|
|
135
|
+
{i18n.t("tools.notDiscovered")}
|
|
136
|
+
</p>
|
|
137
|
+
) : (
|
|
138
|
+
<ul className="divide-y">
|
|
139
|
+
{entries.map((entry) => {
|
|
140
|
+
const Status = statusIcon[entry.connectionStatus];
|
|
141
|
+
return (
|
|
142
|
+
<li key={entry.capability.name}>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={() => setSelected(entry)}
|
|
146
|
+
className="flex w-full items-start justify-between gap-5 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
147
|
+
>
|
|
148
|
+
<span className="min-w-0">
|
|
149
|
+
<span className="block text-sm font-medium">
|
|
150
|
+
{entry.capability.title ?? entry.capability.name}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
153
|
+
{entry.capability.name}
|
|
154
|
+
</span>
|
|
155
|
+
{entry.capability.description && (
|
|
156
|
+
<span className="mt-2 line-clamp-2 block text-xs text-muted-foreground">
|
|
157
|
+
{entry.capability.description}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</span>
|
|
161
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted px-2 py-1 text-xs text-muted-foreground">
|
|
162
|
+
<Status aria-hidden="true" className="size-3.5" />
|
|
163
|
+
{i18n.t(`tools.status.${entry.connectionStatus}`)}
|
|
164
|
+
</span>
|
|
165
|
+
</button>
|
|
166
|
+
</li>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</ul>
|
|
170
|
+
)}
|
|
171
|
+
</section>
|
|
172
|
+
))
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<ToolTester entry={selected} />
|
|
176
|
+
</div>
|
|
177
|
+
{registering && <RegisterSource close={() => setRegistering(false)} />}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
|
|
183
|
+
const { data, i18n } = useIntelRouterContext();
|
|
184
|
+
const [argumentsText, setArgumentsText] = useState("{}");
|
|
185
|
+
const [parseError, setParseError] = useState(false);
|
|
186
|
+
const safeToTest =
|
|
187
|
+
entry?.capability.annotations.readOnlyHint === true &&
|
|
188
|
+
entry.capability.annotations.destructiveHint !== true;
|
|
189
|
+
const test = useMutation({
|
|
190
|
+
mutationFn: async () => {
|
|
191
|
+
const parsed: unknown = JSON.parse(argumentsText);
|
|
192
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("object");
|
|
193
|
+
return await data.testTool({
|
|
194
|
+
sourceId: entry?.source.id ?? "",
|
|
195
|
+
name: entry?.capability.name ?? "",
|
|
196
|
+
arguments: parsed as Record<string, unknown>,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
onMutate: () => setParseError(false),
|
|
200
|
+
onError: (error) => setParseError(error instanceof SyntaxError || error.message === "object"),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<aside className="sticky top-5 h-fit rounded-xl border bg-card p-5 shadow-sm">
|
|
205
|
+
<h2 className="font-semibold">{i18n.t("tools.testTitle")}</h2>
|
|
206
|
+
{!entry ? (
|
|
207
|
+
<p className="mt-3 text-sm text-muted-foreground">{i18n.t("tools.select")}</p>
|
|
208
|
+
) : (
|
|
209
|
+
<div className="mt-4 space-y-4">
|
|
210
|
+
<div>
|
|
211
|
+
<p className="text-sm font-medium">{entry.capability.title ?? entry.capability.name}</p>
|
|
212
|
+
<p className="mt-1 font-mono text-xs text-muted-foreground">{entry.capability.name}</p>
|
|
213
|
+
</div>
|
|
214
|
+
{entry.capability.annotations.destructiveHint && (
|
|
215
|
+
<p className="flex gap-2 rounded-md bg-destructive/10 p-3 text-xs text-destructive">
|
|
216
|
+
<AlertTriangle aria-hidden="true" className="size-4 shrink-0" />
|
|
217
|
+
{i18n.t("tools.destructive")}
|
|
218
|
+
</p>
|
|
219
|
+
)}
|
|
220
|
+
{!safeToTest && (
|
|
221
|
+
<p className="flex gap-2 rounded-md bg-muted p-3 text-xs text-muted-foreground">
|
|
222
|
+
<ShieldAlert aria-hidden="true" className="size-4 shrink-0" />
|
|
223
|
+
{i18n.t("tools.testUnsafe")}
|
|
224
|
+
</p>
|
|
225
|
+
)}
|
|
226
|
+
{entry.connectionStatus !== "connected" && (
|
|
227
|
+
<p className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
|
|
228
|
+
{i18n.t("tools.connectionRequired")}
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
<label className="block text-xs font-medium">
|
|
232
|
+
{i18n.t("tools.arguments")}
|
|
233
|
+
<textarea
|
|
234
|
+
value={argumentsText}
|
|
235
|
+
onChange={(event) => setArgumentsText(event.target.value)}
|
|
236
|
+
rows={9}
|
|
237
|
+
spellCheck={false}
|
|
238
|
+
className="mt-2 w-full resize-y rounded-md border bg-background p-3 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
239
|
+
/>
|
|
240
|
+
</label>
|
|
241
|
+
{parseError && <p className="text-xs text-destructive">{i18n.t("tools.invalidJson")}</p>}
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
disabled={test.isPending || entry.connectionStatus !== "connected" || !safeToTest}
|
|
245
|
+
onClick={() => test.mutate()}
|
|
246
|
+
className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
247
|
+
>
|
|
248
|
+
<Play aria-hidden="true" className="size-4" />
|
|
249
|
+
{i18n.t("tools.runTest")}
|
|
250
|
+
</button>
|
|
251
|
+
{test.data && (
|
|
252
|
+
<pre className="max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
253
|
+
{JSON.stringify(test.data, null, 2)}
|
|
254
|
+
</pre>
|
|
255
|
+
)}
|
|
256
|
+
{test.isError && !parseError && (
|
|
257
|
+
<p className="text-xs text-destructive">{i18n.t("common.unavailable")}</p>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</aside>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function RegisterSource({ close }: { close(): void }) {
|
|
266
|
+
const { data, i18n } = useIntelRouterContext();
|
|
267
|
+
const queryClient = useQueryClient();
|
|
268
|
+
const [name, setName] = useState("");
|
|
269
|
+
const [url, setUrl] = useState("");
|
|
270
|
+
const [connectionHandle, setConnectionHandle] = useState("intel-tools");
|
|
271
|
+
const create = useMutation({
|
|
272
|
+
mutationFn: () =>
|
|
273
|
+
data.createToolSource({
|
|
274
|
+
name,
|
|
275
|
+
url,
|
|
276
|
+
connectionHandle,
|
|
277
|
+
idempotencyKey: crypto.randomUUID(),
|
|
278
|
+
}),
|
|
279
|
+
onSuccess: async () => {
|
|
280
|
+
await queryClient.invalidateQueries({ queryKey: ["tool-sources"] });
|
|
281
|
+
close();
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
return (
|
|
285
|
+
<Modal title={i18n.t("tools.addSource")} close={close} className="max-w-lg">
|
|
286
|
+
<form
|
|
287
|
+
className="space-y-4"
|
|
288
|
+
onSubmit={(event) => {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
if (create.isPending) return;
|
|
291
|
+
create.mutate();
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<Field label={i18n.t("common.title")} value={name} setValue={setName} />
|
|
295
|
+
<Field label={i18n.t("tools.endpoint")} value={url} setValue={setUrl} type="url" />
|
|
296
|
+
<Field
|
|
297
|
+
label={i18n.t("tools.connectionHandle")}
|
|
298
|
+
value={connectionHandle}
|
|
299
|
+
setValue={setConnectionHandle}
|
|
300
|
+
pattern="[a-z0-9][a-z0-9-]{1,62}"
|
|
301
|
+
/>
|
|
302
|
+
<p className="text-xs text-muted-foreground">{i18n.t("tools.sourceHelp")}</p>
|
|
303
|
+
{create.isError && (
|
|
304
|
+
<p role="alert" className="text-sm text-destructive">
|
|
305
|
+
{i18n.t("tools.operationFailed")}
|
|
306
|
+
</p>
|
|
307
|
+
)}
|
|
308
|
+
<button
|
|
309
|
+
type="submit"
|
|
310
|
+
disabled={create.isPending}
|
|
311
|
+
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
312
|
+
>
|
|
313
|
+
{create.isPending ? i18n.t("common.saving") : i18n.t("common.create")}
|
|
314
|
+
</button>
|
|
315
|
+
</form>
|
|
316
|
+
</Modal>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function Field({
|
|
321
|
+
label,
|
|
322
|
+
value,
|
|
323
|
+
setValue,
|
|
324
|
+
type = "text",
|
|
325
|
+
pattern,
|
|
326
|
+
}: {
|
|
327
|
+
label: string;
|
|
328
|
+
value: string;
|
|
329
|
+
setValue(value: string): void;
|
|
330
|
+
type?: string;
|
|
331
|
+
pattern?: string;
|
|
332
|
+
}) {
|
|
333
|
+
return (
|
|
334
|
+
<label className="block text-sm font-medium">
|
|
335
|
+
{label}
|
|
336
|
+
<input
|
|
337
|
+
required
|
|
338
|
+
type={type}
|
|
339
|
+
pattern={pattern}
|
|
340
|
+
value={value}
|
|
341
|
+
onChange={(event) => setValue(event.target.value)}
|
|
342
|
+
className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
343
|
+
/>
|
|
344
|
+
</label>
|
|
345
|
+
);
|
|
346
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"paths": { "@/*": ["src/*"] },
|
|
8
|
+
"types": ["vite/client"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
|
|
11
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { defineConfig } from "vite";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
|
9
|
+
server: {
|
|
10
|
+
proxy: {
|
|
11
|
+
"/api": { target: "http://localhost:8788", changeOrigin: true },
|
|
12
|
+
"/auth": { target: "http://localhost:8788", changeOrigin: true },
|
|
13
|
+
"/mcp": { target: "http://localhost:8788", changeOrigin: true },
|
|
14
|
+
"/.well-known": { target: "http://localhost:8788", changeOrigin: true },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|