@anchrd/intel-ui 0.1.1 → 0.2.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/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @anchrd/intel-ui
|
|
2
2
|
|
|
3
3
|
The Intel browser application: Knowledge, Flows and Tools for the
|
|
4
|
-
[`@anchrd/intel`](https://www.npmjs.com/package/@anchrd/intel) server.
|
|
4
|
+
[`@anchrd/intel-api`](https://www.npmjs.com/package/@anchrd/intel-api) server.
|
|
5
5
|
|
|
6
6
|
This package ships **source, not a bundle**. You compile it yourself so your own theme, logo and
|
|
7
7
|
languages are applied at build time and the output belongs to you — no customer build artifact ever
|
|
@@ -9,10 +9,10 @@ lives in `node_modules`.
|
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
|
-
Install it next to `@anchrd/intel`, then let the CLI build it:
|
|
12
|
+
Install it next to `@anchrd/intel-api`, then let the CLI build it:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npm install @anchrd/intel @anchrd/intel-ui
|
|
15
|
+
npm install @anchrd/intel-api @anchrd/intel-ui
|
|
16
16
|
npx intel build
|
|
17
17
|
```
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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";
|
|
@@ -49,6 +45,10 @@ const DeleteResult = z.strictObject({ deleted: z.boolean() });
|
|
|
49
45
|
// Attachment uploads and downloads share this budget, so it is generous rather than snappy.
|
|
50
46
|
const RequestTimeoutMs = 60_000;
|
|
51
47
|
|
|
48
|
+
// Deeper nesting than this is not a tree anyone navigates; the limit exists to bound the request
|
|
49
|
+
// fan-out on first paint, not to express a product rule.
|
|
50
|
+
const MaxTreeDepth = 12;
|
|
51
|
+
|
|
52
52
|
// The Worker serves the login route, so the path belongs to the data layer rather than to any view.
|
|
53
53
|
export function loginPath(returnTo?: string): string {
|
|
54
54
|
return returnTo ? `/auth/login?returnTo=${encodeURIComponent(returnTo)}` : "/auth/login";
|
|
@@ -115,12 +115,23 @@ export function createIntelDataProvider(
|
|
|
115
115
|
return await request(`/knowledge?${params}`, KnowledgeNodeList);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
// One request per folder, so a deep tree is an N+1 on the first paint. The depth limit bounds that
|
|
119
|
+
// and, more importantly, makes a cyclic parentId impossible to hang on: without it a cycle would
|
|
120
|
+
// recurse until the tab dies. `seen` stops a cycle at the first repeat rather than at the limit.
|
|
121
|
+
async function loadChildren(
|
|
122
|
+
parentId: string | null,
|
|
123
|
+
depth = 0,
|
|
124
|
+
seen: ReadonlySet<string> = new Set(),
|
|
125
|
+
): Promise<KnowledgeTreeNode[]> {
|
|
126
|
+
if (depth >= MaxTreeDepth) return [];
|
|
119
127
|
const { items } = await listKnowledge({ parentId });
|
|
120
128
|
return await Promise.all(
|
|
121
129
|
items.map(async (node) => ({
|
|
122
130
|
...node,
|
|
123
|
-
children:
|
|
131
|
+
children:
|
|
132
|
+
node.kind === "folder" && !seen.has(node.id)
|
|
133
|
+
? await loadChildren(node.id, depth + 1, new Set([...seen, node.id]))
|
|
134
|
+
: [],
|
|
124
135
|
})),
|
|
125
136
|
);
|
|
126
137
|
}
|
|
@@ -294,26 +305,6 @@ export function createIntelDataProvider(
|
|
|
294
305
|
body: JSON.stringify(parsed),
|
|
295
306
|
});
|
|
296
307
|
},
|
|
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
308
|
async listTools() {
|
|
318
309
|
return await request("/tools", ToolCatalog);
|
|
319
310
|
},
|
|
@@ -324,8 +315,8 @@ export function createIntelDataProvider(
|
|
|
324
315
|
});
|
|
325
316
|
},
|
|
326
317
|
|
|
327
|
-
|
|
328
|
-
return `${baseUrl}/auth/connect
|
|
318
|
+
portalConnectUrl(returnTo = "/tools") {
|
|
319
|
+
return `${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}`;
|
|
329
320
|
},
|
|
330
321
|
async logout() {
|
|
331
322
|
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
|
-
}
|