@anchrd/intel-ui 0.1.0 → 0.2.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
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
# @anchrd/intel-ui
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The Intel browser application: Knowledge, Flows and Tools for the
|
|
4
|
+
[`@anchrd/intel-api`](https://www.npmjs.com/package/@anchrd/intel-api) server.
|
|
5
|
+
|
|
6
|
+
This package ships **source, not a bundle**. You compile it yourself so your own theme, logo and
|
|
7
|
+
languages are applied at build time and the output belongs to you — no customer build artifact ever
|
|
8
|
+
lives in `node_modules`.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Install it next to `@anchrd/intel-api`, then let the CLI build it:
|
|
4
13
|
|
|
5
14
|
```bash
|
|
15
|
+
npm install @anchrd/intel-api @anchrd/intel-ui
|
|
6
16
|
npx intel build
|
|
7
17
|
```
|
|
8
18
|
|
|
9
|
-
The CLI applies `intel.json`
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
The CLI applies `intel.json` and writes the static output to `.intel/ui`, which the Intel Worker
|
|
20
|
+
serves through its `ASSETS` binding.
|
|
21
|
+
|
|
22
|
+
## Theming
|
|
23
|
+
|
|
24
|
+
Theme CSS overrides shadcn semantic variables such as `--primary`, `--background`, `--sidebar` and
|
|
25
|
+
`--radius`. It loads after Intel's defaults, so customer tokens win without forking a single React
|
|
26
|
+
component. Every product component uses semantic tokens rather than palette colours, which is
|
|
27
|
+
enforced by a lint rule.
|
|
28
|
+
|
|
29
|
+
Point `intel.json` at your files:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"ui": {
|
|
34
|
+
"theme": "./branding/theme.css",
|
|
35
|
+
"logo": "./branding/logo.svg",
|
|
36
|
+
"favicon": "./branding/favicon.svg",
|
|
37
|
+
"defaultLanguage": "en",
|
|
38
|
+
"languages": {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Missing assets, non-SVG branding and incomplete language catalogs fail the build rather than
|
|
44
|
+
silently producing a half-branded application.
|
|
45
|
+
|
|
46
|
+
## What is inside
|
|
47
|
+
|
|
48
|
+
Vite, React, TanStack Router and Query, with shadcn components. Knowledge uses BlockNote for editing
|
|
49
|
+
and Sigma for the graph, Flows uses React Flow, and Tools presents the discovered MCP catalog.
|
|
50
|
+
Route-level code splitting keeps both editors out of the initial bundle.
|
|
51
|
+
|
|
52
|
+
Server state lives only in TanStack Query, and one data provider is the sole module that talks to the
|
|
53
|
+
Intel HTTP surface. The application uses same-origin `HttpOnly` sessions established by the Intel
|
|
54
|
+
Worker against Gate OAuth: it holds no service key and stores no access token in the browser.
|
|
12
55
|
|
|
13
|
-
|
|
14
|
-
Gate OAuth. It contains no service key or browser-side access-token storage.
|
|
56
|
+
## License
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
uses shadcn semantic tokens so an `intel.json` theme can replace the customer design safely.
|
|
58
|
+
Proprietary. Published publicly for installation convenience; this is not an open-source license and
|
|
59
|
+
grants no right to use, copy, modify or redistribute the software. Contact Anchrd for licensing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anchrd/intel-contract": "^0.
|
|
31
|
+
"@anchrd/intel-contract": "^0.2.0",
|
|
32
32
|
"@blocknote/core": "^0.52.1",
|
|
33
33
|
"@blocknote/react": "^0.52.1",
|
|
34
34
|
"@blocknote/shadcn": "^0.52.1",
|
|
@@ -4,9 +4,7 @@ import {
|
|
|
4
4
|
CreateFlowInput,
|
|
5
5
|
CreateKnowledgeLinkInput,
|
|
6
6
|
CreateKnowledgeNodeInput,
|
|
7
|
-
CreateToolSourceInput,
|
|
8
7
|
DeleteKnowledgeLinkInput,
|
|
9
|
-
DiscoverToolSourceInput,
|
|
10
8
|
Flow,
|
|
11
9
|
FlowDocument,
|
|
12
10
|
FlowList,
|
|
@@ -36,8 +34,6 @@ import {
|
|
|
36
34
|
StartFlowRunInput,
|
|
37
35
|
TestToolInput,
|
|
38
36
|
ToolCatalog,
|
|
39
|
-
ToolSource,
|
|
40
|
-
ToolSourceList,
|
|
41
37
|
ToolTestResult,
|
|
42
38
|
UpdateKnowledgeNodeInput,
|
|
43
39
|
} from "@anchrd/intel-contract";
|
|
@@ -294,26 +290,6 @@ export function createIntelDataProvider(
|
|
|
294
290
|
body: JSON.stringify(parsed),
|
|
295
291
|
});
|
|
296
292
|
},
|
|
297
|
-
async listToolSources() {
|
|
298
|
-
return await request("/tool-sources", ToolSourceList);
|
|
299
|
-
},
|
|
300
|
-
async createToolSource(input) {
|
|
301
|
-
return await request("/tool-sources", ToolSource, {
|
|
302
|
-
method: "POST",
|
|
303
|
-
body: JSON.stringify(CreateToolSourceInput.parse(input)),
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
async discoverToolSource(input) {
|
|
307
|
-
const parsed = DiscoverToolSourceInput.parse(input);
|
|
308
|
-
return await request(
|
|
309
|
-
`/tool-sources/${encodeURIComponent(parsed.sourceId)}/discover`,
|
|
310
|
-
ToolCatalog,
|
|
311
|
-
{
|
|
312
|
-
method: "POST",
|
|
313
|
-
body: JSON.stringify(parsed),
|
|
314
|
-
},
|
|
315
|
-
);
|
|
316
|
-
},
|
|
317
293
|
async listTools() {
|
|
318
294
|
return await request("/tools", ToolCatalog);
|
|
319
295
|
},
|
|
@@ -324,8 +300,8 @@ export function createIntelDataProvider(
|
|
|
324
300
|
});
|
|
325
301
|
},
|
|
326
302
|
|
|
327
|
-
|
|
328
|
-
return `${baseUrl}/auth/connect
|
|
303
|
+
portalConnectUrl(returnTo = "/tools") {
|
|
304
|
+
return `${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}`;
|
|
329
305
|
},
|
|
330
306
|
async logout() {
|
|
331
307
|
const response = await doFetch(`${baseUrl}/auth/logout`, {
|
|
@@ -4,9 +4,7 @@ import type {
|
|
|
4
4
|
CreateFlowInput,
|
|
5
5
|
CreateKnowledgeLinkInput,
|
|
6
6
|
CreateKnowledgeNodeInput,
|
|
7
|
-
CreateToolSourceInput,
|
|
8
7
|
DeleteKnowledgeLinkInput,
|
|
9
|
-
DiscoverToolSourceInput,
|
|
10
8
|
Flow,
|
|
11
9
|
FlowDocument,
|
|
12
10
|
FlowList,
|
|
@@ -35,8 +33,6 @@ import type {
|
|
|
35
33
|
StartFlowRunInput,
|
|
36
34
|
TestToolInput,
|
|
37
35
|
ToolCatalog,
|
|
38
|
-
ToolSource,
|
|
39
|
-
ToolSourceList,
|
|
40
36
|
ToolTestResult,
|
|
41
37
|
UpdateKnowledgeNodeInput,
|
|
42
38
|
} from "@anchrd/intel-contract";
|
|
@@ -57,7 +53,7 @@ export interface IntelDataProvider {
|
|
|
57
53
|
saveKnowledge(input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
|
|
58
54
|
saveKnowledgeAttachment(input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
|
|
59
55
|
getKnowledgeAttachment(nodeId: string): Promise<Blob>;
|
|
60
|
-
|
|
56
|
+
portalConnectUrl(returnTo?: string): string;
|
|
61
57
|
listKnowledgeVersions(nodeId: string): Promise<KnowledgeVersionList>;
|
|
62
58
|
updateKnowledge(input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
63
59
|
archiveKnowledge(input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
@@ -76,9 +72,6 @@ export interface IntelDataProvider {
|
|
|
76
72
|
startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
|
|
77
73
|
getFlowRun(runId: string): Promise<FlowRunStep>;
|
|
78
74
|
completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
|
|
79
|
-
listToolSources(): Promise<ToolSourceList>;
|
|
80
|
-
createToolSource(input: CreateToolSourceInput): Promise<ToolSource>;
|
|
81
|
-
discoverToolSource(input: DiscoverToolSourceInput): Promise<ToolCatalog>;
|
|
82
75
|
listTools(): Promise<ToolCatalog>;
|
|
83
76
|
testTool(input: TestToolInput): Promise<ToolTestResult>;
|
|
84
77
|
logout(): Promise<void>;
|
package/src/flows/flows.tsx
CHANGED
|
@@ -198,7 +198,6 @@ function newNode(
|
|
|
198
198
|
...common,
|
|
199
199
|
kind,
|
|
200
200
|
configuration: {
|
|
201
|
-
sourceId: "select-a-source",
|
|
202
201
|
toolName: "select-a-tool",
|
|
203
202
|
fingerprint: null,
|
|
204
203
|
arguments: {},
|
|
@@ -599,10 +598,7 @@ function NodeInspector({
|
|
|
599
598
|
}: {
|
|
600
599
|
node: CanvasNode | null;
|
|
601
600
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
602
|
-
tools: Array<{
|
|
603
|
-
source: { id: string; name: string };
|
|
604
|
-
capability: { name: string; title: string | null; fingerprint: string };
|
|
605
|
-
}>;
|
|
601
|
+
tools: Array<{ name: string; title: string | null; fingerprint: string }>;
|
|
606
602
|
knowledge: KnowledgeTreeNode[];
|
|
607
603
|
}) {
|
|
608
604
|
const { i18n } = useIntelRouterContext();
|
|
@@ -726,23 +722,18 @@ function NodeInspector({
|
|
|
726
722
|
<label className="block text-sm font-medium">
|
|
727
723
|
{i18n.t("flows.tool")}
|
|
728
724
|
<select
|
|
729
|
-
value={
|
|
725
|
+
value={contract.configuration.toolName}
|
|
730
726
|
onChange={(event) => {
|
|
731
|
-
const
|
|
732
|
-
const toolName = name.join(":");
|
|
727
|
+
const toolName = event.target.value;
|
|
733
728
|
update((value) =>
|
|
734
|
-
value.kind === "tool"
|
|
729
|
+
value.kind === "tool"
|
|
735
730
|
? {
|
|
736
731
|
...value,
|
|
737
732
|
configuration: {
|
|
738
733
|
...value.configuration,
|
|
739
|
-
sourceId,
|
|
740
734
|
toolName,
|
|
741
735
|
fingerprint:
|
|
742
|
-
tools.find(
|
|
743
|
-
(entry) =>
|
|
744
|
-
entry.source.id === sourceId && entry.capability.name === toolName,
|
|
745
|
-
)?.capability.fingerprint ?? null,
|
|
736
|
+
tools.find((entry) => entry.name === toolName)?.fingerprint ?? null,
|
|
746
737
|
},
|
|
747
738
|
}
|
|
748
739
|
: value,
|
|
@@ -750,13 +741,10 @@ function NodeInspector({
|
|
|
750
741
|
}}
|
|
751
742
|
className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
752
743
|
>
|
|
753
|
-
<option value="select-a-
|
|
744
|
+
<option value="select-a-tool">{i18n.t("flows.selectTool")}</option>
|
|
754
745
|
{tools.map((entry) => (
|
|
755
|
-
<option
|
|
756
|
-
|
|
757
|
-
value={`${entry.source.id}:${entry.capability.name}`}
|
|
758
|
-
>
|
|
759
|
-
{entry.source.name} · {entry.capability.title ?? entry.capability.name}
|
|
746
|
+
<option key={entry.name} value={entry.name}>
|
|
747
|
+
{entry.title ?? entry.name}
|
|
760
748
|
</option>
|
|
761
749
|
))}
|
|
762
750
|
</select>
|
package/src/i18n/en.json
CHANGED
|
@@ -57,30 +57,23 @@
|
|
|
57
57
|
"knowledge.saveConflict": "This document changed elsewhere. Reload it before saving again.",
|
|
58
58
|
"knowledge.saveError": "This document could not be saved. Reload and try again.",
|
|
59
59
|
"tools.title": "Company tools",
|
|
60
|
-
"tools.description": "
|
|
61
|
-
"tools.
|
|
62
|
-
"tools.
|
|
63
|
-
"tools.
|
|
64
|
-
"tools.
|
|
65
|
-
"tools.
|
|
66
|
-
"tools.
|
|
67
|
-
"tools.sourceHelp": "Intel registers the handle/connect interface in Gate. Grant it to the right roles and keep the credential in Gate.",
|
|
68
|
-
"tools.discover": "Discover tools",
|
|
69
|
-
"tools.notDiscovered": "No tools cached yet. Connect the source in Gate, then run discovery.",
|
|
60
|
+
"tools.description": "The MCP tools you can reach through the company portal.",
|
|
61
|
+
"tools.connect": "Connect the portal",
|
|
62
|
+
"tools.reconnect": "Reconnect",
|
|
63
|
+
"tools.disconnected": "Portal not connected",
|
|
64
|
+
"tools.disconnectedHelp": "Sign in to the company MCP portal once. The portal decides which servers you may use and holds their credentials; Intel never sees them.",
|
|
65
|
+
"tools.empty": "No tools available to you",
|
|
66
|
+
"tools.emptyHelp": "The portal returned no tools for your account. An administrator configures which servers it offers.",
|
|
70
67
|
"tools.testTitle": "Tool playground",
|
|
71
|
-
"tools.select": "Select a
|
|
68
|
+
"tools.select": "Select a tool to inspect and test it.",
|
|
72
69
|
"tools.arguments": "Arguments (JSON object)",
|
|
73
70
|
"tools.runTest": "Run controlled test",
|
|
74
71
|
"tools.invalidJson": "Arguments must be a valid JSON object.",
|
|
75
|
-
"tools.destructive": "
|
|
76
|
-
"tools.
|
|
72
|
+
"tools.destructive": "The portal marks this tool as potentially destructive. Review the arguments before running it.",
|
|
73
|
+
"tools.destructiveShort": "Destructive",
|
|
77
74
|
"tools.testUnsafe": "The playground only runs tools explicitly marked read-only and non-destructive.",
|
|
78
|
-
"tools.operationFailed": "The
|
|
79
|
-
"tools.connectFailed": "The
|
|
80
|
-
"tools.status.connected": "Connected",
|
|
81
|
-
"tools.status.missing": "Not connected",
|
|
82
|
-
"tools.status.forbidden": "No access",
|
|
83
|
-
"tools.status.invalid": "Invalid",
|
|
75
|
+
"tools.operationFailed": "The tool list could not be loaded. Reconnect the portal and try again.",
|
|
76
|
+
"tools.connectFailed": "The portal could not be connected with your current access.",
|
|
84
77
|
"flows.title": "Company flows",
|
|
85
78
|
"flows.description": "Model-agnostic processes that connect instructions, Knowledge, tools, and people.",
|
|
86
79
|
"flows.new": "New flow",
|
package/src/tools/tools.tsx
CHANGED
|
@@ -1,56 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { useMutation, useQuery
|
|
3
|
-
import {
|
|
4
|
-
|
|
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";
|
|
1
|
+
import type { ToolCapability } from "@anchrd/intel-contract";
|
|
2
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { AlertTriangle, LogIn, Play, ShieldAlert, Wrench } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
17
5
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
18
6
|
|
|
19
|
-
const statusIcon = {
|
|
20
|
-
connected: CheckCircle2,
|
|
21
|
-
missing: KeyRound,
|
|
22
|
-
forbidden: ShieldAlert,
|
|
23
|
-
invalid: CircleOff,
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
7
|
export function Tools() {
|
|
27
8
|
const { data, i18n } = useIntelRouterContext();
|
|
28
|
-
const queryClient = useQueryClient();
|
|
29
|
-
const sources = useQuery({ queryKey: ["tool-sources"], queryFn: () => data.listToolSources() });
|
|
30
9
|
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
31
|
-
const [
|
|
32
|
-
const [selected, setSelected] = useState<ToolCatalogEntry | null>(null);
|
|
10
|
+
const [selected, setSelected] = useState<ToolCapability | null>(null);
|
|
33
11
|
const connectError =
|
|
34
12
|
typeof window === "undefined"
|
|
35
13
|
? null
|
|
36
14
|
: 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
15
|
|
|
55
16
|
return (
|
|
56
17
|
<div className="min-h-screen">
|
|
@@ -59,26 +20,19 @@ export function Tools() {
|
|
|
59
20
|
<h1 className="text-xl font-semibold tracking-tight">{i18n.t("tools.title")}</h1>
|
|
60
21
|
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.description")}</p>
|
|
61
22
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
23
|
+
{catalog.data?.portalConnected && (
|
|
24
|
+
<a
|
|
25
|
+
href={data.portalConnectUrl("/tools")}
|
|
26
|
+
className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
27
|
+
>
|
|
28
|
+
<LogIn aria-hidden="true" className="size-4" />
|
|
29
|
+
{i18n.t("tools.reconnect")}
|
|
30
|
+
</a>
|
|
31
|
+
)}
|
|
70
32
|
</header>
|
|
71
33
|
|
|
72
34
|
<div className="grid gap-5 p-8 xl:grid-cols-[minmax(0,1fr)_24rem]">
|
|
73
35
|
<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
36
|
{connectError && (
|
|
83
37
|
<p
|
|
84
38
|
role="alert"
|
|
@@ -87,9 +41,28 @@ export function Tools() {
|
|
|
87
41
|
{i18n.t("tools.connectFailed")}
|
|
88
42
|
</p>
|
|
89
43
|
)}
|
|
90
|
-
{
|
|
44
|
+
{catalog.isPending ? (
|
|
91
45
|
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
92
|
-
) :
|
|
46
|
+
) : catalog.isError ? (
|
|
47
|
+
<p role="alert" className="text-sm text-destructive">
|
|
48
|
+
{i18n.t("tools.operationFailed")}
|
|
49
|
+
</p>
|
|
50
|
+
) : !catalog.data?.portalConnected ? (
|
|
51
|
+
<section className="rounded-xl border border-dashed bg-card p-12 text-center">
|
|
52
|
+
<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />
|
|
53
|
+
<h2 className="mt-4 font-semibold">{i18n.t("tools.disconnected")}</h2>
|
|
54
|
+
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">
|
|
55
|
+
{i18n.t("tools.disconnectedHelp")}
|
|
56
|
+
</p>
|
|
57
|
+
<a
|
|
58
|
+
href={data.portalConnectUrl("/tools")}
|
|
59
|
+
className="mt-5 inline-flex items-center gap-2 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"
|
|
60
|
+
>
|
|
61
|
+
<LogIn aria-hidden="true" className="size-4" />
|
|
62
|
+
{i18n.t("tools.connect")}
|
|
63
|
+
</a>
|
|
64
|
+
</section>
|
|
65
|
+
) : catalog.data.items.length === 0 ? (
|
|
93
66
|
<section className="rounded-xl border border-dashed bg-card p-12 text-center">
|
|
94
67
|
<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />
|
|
95
68
|
<h2 className="mt-4 font-semibold">{i18n.t("tools.empty")}</h2>
|
|
@@ -98,120 +71,81 @@ export function Tools() {
|
|
|
98
71
|
</p>
|
|
99
72
|
</section>
|
|
100
73
|
) : (
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
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>
|
|
74
|
+
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
75
|
+
<ul className="divide-y">
|
|
76
|
+
{catalog.data.items.map((capability) => (
|
|
77
|
+
<li key={capability.name}>
|
|
122
78
|
<button
|
|
123
79
|
type="button"
|
|
124
|
-
|
|
125
|
-
|
|
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"
|
|
80
|
+
onClick={() => setSelected(capability)}
|
|
81
|
+
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"
|
|
127
82
|
>
|
|
128
|
-
<
|
|
129
|
-
|
|
83
|
+
<span className="min-w-0">
|
|
84
|
+
<span className="block text-sm font-medium">
|
|
85
|
+
{capability.title ?? capability.name}
|
|
86
|
+
</span>
|
|
87
|
+
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
88
|
+
{capability.name}
|
|
89
|
+
</span>
|
|
90
|
+
{capability.description && (
|
|
91
|
+
<span className="mt-2 line-clamp-2 block text-xs text-muted-foreground">
|
|
92
|
+
{capability.description}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</span>
|
|
96
|
+
{capability.annotations.destructiveHint && (
|
|
97
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-1 text-xs text-destructive">
|
|
98
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
99
|
+
{i18n.t("tools.destructiveShort")}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
130
102
|
</button>
|
|
131
|
-
</
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
))
|
|
103
|
+
</li>
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
</section>
|
|
173
107
|
)}
|
|
174
108
|
</div>
|
|
175
|
-
<ToolTester
|
|
109
|
+
<ToolTester capability={selected} />
|
|
176
110
|
</div>
|
|
177
|
-
{registering && <RegisterSource close={() => setRegistering(false)} />}
|
|
178
111
|
</div>
|
|
179
112
|
);
|
|
180
113
|
}
|
|
181
114
|
|
|
182
|
-
function ToolTester({
|
|
115
|
+
function ToolTester({ capability }: { capability: ToolCapability | null }) {
|
|
183
116
|
const { data, i18n } = useIntelRouterContext();
|
|
184
117
|
const [argumentsText, setArgumentsText] = useState("{}");
|
|
185
118
|
const [parseError, setParseError] = useState(false);
|
|
186
119
|
const safeToTest =
|
|
187
|
-
|
|
188
|
-
|
|
120
|
+
capability?.annotations.readOnlyHint === true &&
|
|
121
|
+
capability.annotations.destructiveHint !== true;
|
|
189
122
|
const test = useMutation({
|
|
190
123
|
mutationFn: async () => {
|
|
191
124
|
const parsed: unknown = JSON.parse(argumentsText);
|
|
192
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
125
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
126
|
+
throw new SyntaxError("Arguments must be a JSON object");
|
|
127
|
+
}
|
|
193
128
|
return await data.testTool({
|
|
194
|
-
|
|
195
|
-
name: entry?.capability.name ?? "",
|
|
129
|
+
name: capability?.name ?? "",
|
|
196
130
|
arguments: parsed as Record<string, unknown>,
|
|
197
131
|
});
|
|
198
132
|
},
|
|
199
133
|
onMutate: () => setParseError(false),
|
|
200
|
-
onError: (error) => setParseError(error instanceof SyntaxError
|
|
134
|
+
onError: (error) => setParseError(error instanceof SyntaxError),
|
|
201
135
|
});
|
|
202
136
|
|
|
203
137
|
return (
|
|
204
138
|
<aside className="sticky top-5 h-fit rounded-xl border bg-card p-5 shadow-sm">
|
|
205
139
|
<h2 className="font-semibold">{i18n.t("tools.testTitle")}</h2>
|
|
206
|
-
{!
|
|
140
|
+
{!capability ? (
|
|
207
141
|
<p className="mt-3 text-sm text-muted-foreground">{i18n.t("tools.select")}</p>
|
|
208
142
|
) : (
|
|
209
143
|
<div className="mt-4 space-y-4">
|
|
210
144
|
<div>
|
|
211
|
-
<p className="text-sm font-medium">{
|
|
212
|
-
<p className="mt-1 font-mono text-xs text-muted-foreground">{
|
|
145
|
+
<p className="text-sm font-medium">{capability.title ?? capability.name}</p>
|
|
146
|
+
<p className="mt-1 font-mono text-xs text-muted-foreground">{capability.name}</p>
|
|
213
147
|
</div>
|
|
214
|
-
{
|
|
148
|
+
{capability.annotations.destructiveHint && (
|
|
215
149
|
<p className="flex gap-2 rounded-md bg-destructive/10 p-3 text-xs text-destructive">
|
|
216
150
|
<AlertTriangle aria-hidden="true" className="size-4 shrink-0" />
|
|
217
151
|
{i18n.t("tools.destructive")}
|
|
@@ -223,11 +157,6 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
|
|
|
223
157
|
{i18n.t("tools.testUnsafe")}
|
|
224
158
|
</p>
|
|
225
159
|
)}
|
|
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
160
|
<label className="block text-xs font-medium">
|
|
232
161
|
{i18n.t("tools.arguments")}
|
|
233
162
|
<textarea
|
|
@@ -241,7 +170,7 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
|
|
|
241
170
|
{parseError && <p className="text-xs text-destructive">{i18n.t("tools.invalidJson")}</p>}
|
|
242
171
|
<button
|
|
243
172
|
type="button"
|
|
244
|
-
disabled={test.isPending ||
|
|
173
|
+
disabled={test.isPending || !safeToTest}
|
|
245
174
|
onClick={() => test.mutate()}
|
|
246
175
|
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
176
|
>
|
|
@@ -261,86 +190,3 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
|
|
|
261
190
|
</aside>
|
|
262
191
|
);
|
|
263
192
|
}
|
|
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
|
-
}
|