@anchrd/intel-ui 0.3.0 → 0.5.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/components.json +7 -1
- package/package.json +3 -1
- package/src/app/action-slot/action-slot.tsx +23 -0
- package/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/src/app/app-tree/app-tree.tsx +798 -0
- package/src/app/app.tsx +99 -54
- package/src/app/header-search/header-search.tsx +291 -0
- package/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/src/app/tree-move/tree-move.tsx +197 -0
- package/src/app/user-footer/user-footer.tsx +103 -0
- package/src/app/view-toggle/view-toggle.tsx +77 -0
- package/src/blocknote-view/blocknote-view.tsx +19 -2
- package/src/branding/favicon.default.svg +2 -2
- package/src/branding/favicon.svg +2 -2
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/collapsible.tsx +20 -0
- package/src/components/ui/command.tsx +160 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +162 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +693 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +170 -85
- package/src/data/intel-data-provider/intel-data-provider.types.ts +64 -20
- package/src/document-link/document-link.tsx +132 -0
- package/src/flow-runs/flow-runs.tsx +225 -0
- package/src/flows/flows.tsx +684 -368
- package/src/flows/node-icon/node-icon.ts +28 -0
- package/src/flows/node-palette/node-palette.tsx +174 -0
- package/src/flows/node-palette/node-palette.types.ts +15 -0
- package/src/graph-pane/graph-pane.tsx +44 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/i18n/en.json +188 -52
- package/src/knowledge/knowledge.tsx +133 -709
- package/src/knowledge-editor/knowledge-editor.tsx +169 -21
- package/src/knowledge-graph/knowledge-graph.ts +26 -24
- package/src/knowledge-graph/knowledge-graph.tsx +33 -24
- package/src/knowledge-table/knowledge-table.tsx +129 -0
- package/src/main.tsx +2 -2
- package/src/resource-menu/resource-menu.tsx +580 -0
- package/src/router/router.tsx +4 -0
- package/src/router/selection-search.ts +43 -0
- package/src/save-button/save-button.tsx +103 -0
- package/src/styles.css +91 -51
- package/src/theme/theme.ts +24 -0
- package/src/tools/tools.tsx +175 -158
package/src/tools/tools.tsx
CHANGED
|
@@ -1,192 +1,209 @@
|
|
|
1
1
|
import type { ToolCapability } from "@anchrd/intel-contract";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
4
|
+
import { AlertTriangle, ChevronRight, LogIn, PlugZap, Wrench } from "lucide-react";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
7
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
8
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
9
|
+
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
5
10
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
11
|
+
import { selectedFrom } from "@/router/selection-search.ts";
|
|
6
12
|
|
|
13
|
+
// The portal namespaces every tool as `<server>__<tool>`, and that prefix is the whole of what
|
|
14
|
+
// Intel ever learns about a tool's origin: the portal resolves the real server and holds its
|
|
15
|
+
// credentials (ADR-0003). A name without a namespace has no origin beyond the portal itself.
|
|
16
|
+
const NamespaceSeparator = "__";
|
|
17
|
+
|
|
18
|
+
function toolOrigin(name: string): string | null {
|
|
19
|
+
const boundary = name.indexOf(NamespaceSeparator);
|
|
20
|
+
return boundary > 0 ? name.slice(0, boundary) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The catalog is one live `tools/list` with the signed-in user's own portal token. Nothing here is
|
|
24
|
+
// stored, mirrored or administered — the screen shows what the portal answers and nothing else.
|
|
7
25
|
export function Tools() {
|
|
8
26
|
const { data, i18n } = useIntelRouterContext();
|
|
9
27
|
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
// A hit from the header search arrives as `?select=<tool name>`, and the row it names opens with
|
|
29
|
+
// the list. Nothing else about the row changes: it is still only a disclosure.
|
|
30
|
+
const requested = useRouterState({ select: (state) => selectedFrom(state.location.search) });
|
|
31
|
+
// The portal connect flow returns here with a marker when it refuses. Only its presence is used:
|
|
32
|
+
// the value is attacker-controlled and could carry provider detail, so it is never rendered.
|
|
33
|
+
const connectFailed =
|
|
34
|
+
typeof window !== "undefined" &&
|
|
35
|
+
new URLSearchParams(window.location.search).has("connectError");
|
|
15
36
|
|
|
16
37
|
return (
|
|
17
|
-
<div className="min-h-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
<h1 className="text-xl font-semibold tracking-tight">{i18n.t("tools.title")}</h1>
|
|
21
|
-
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.description")}</p>
|
|
22
|
-
</div>
|
|
23
|
-
{catalog.data?.portalConnected && (
|
|
38
|
+
<div className="min-h-full p-8">
|
|
39
|
+
{catalog.data?.portalConnected ? (
|
|
40
|
+
<ActionSlot>
|
|
24
41
|
<a
|
|
25
42
|
href={data.portalConnectUrl("/tools")}
|
|
26
|
-
className=
|
|
43
|
+
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
27
44
|
>
|
|
28
|
-
<LogIn aria-hidden="true"
|
|
45
|
+
<LogIn aria-hidden="true" />
|
|
29
46
|
{i18n.t("tools.reconnect")}
|
|
30
47
|
</a>
|
|
48
|
+
</ActionSlot>
|
|
49
|
+
) : null}
|
|
50
|
+
|
|
51
|
+
<div className="mx-auto w-full max-w-4xl space-y-5">
|
|
52
|
+
{connectFailed && (
|
|
53
|
+
<p
|
|
54
|
+
role="alert"
|
|
55
|
+
className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
|
|
56
|
+
>
|
|
57
|
+
{i18n.t("tools.connectFailed")}
|
|
58
|
+
</p>
|
|
31
59
|
)}
|
|
32
|
-
</header>
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<h2 className="mt-4 font-semibold">{i18n.t("tools.empty")}</h2>
|
|
69
|
-
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">
|
|
70
|
-
{i18n.t("tools.emptyHelp")}
|
|
71
|
-
</p>
|
|
72
|
-
</section>
|
|
73
|
-
) : (
|
|
61
|
+
{catalog.isPending ? (
|
|
62
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
63
|
+
) : catalog.isError ? (
|
|
64
|
+
// Unreachable is not empty: the portal did not answer, so Intel knows nothing about this
|
|
65
|
+
// user's tools rather than knowing there are none.
|
|
66
|
+
<Notice
|
|
67
|
+
alert
|
|
68
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
|
|
69
|
+
title={i18n.t("tools.unreachable")}
|
|
70
|
+
help={i18n.t("tools.unreachableHelp")}
|
|
71
|
+
>
|
|
72
|
+
<Button variant="outline" onClick={() => void catalog.refetch()}>
|
|
73
|
+
{i18n.t("common.retry")}
|
|
74
|
+
</Button>
|
|
75
|
+
</Notice>
|
|
76
|
+
) : !catalog.data.portalConnected ? (
|
|
77
|
+
<Notice
|
|
78
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
79
|
+
title={i18n.t("tools.disconnected")}
|
|
80
|
+
help={i18n.t("tools.disconnectedHelp")}
|
|
81
|
+
>
|
|
82
|
+
<a href={data.portalConnectUrl("/tools")} className={buttonVariants()}>
|
|
83
|
+
<LogIn aria-hidden="true" />
|
|
84
|
+
{i18n.t("tools.connect")}
|
|
85
|
+
</a>
|
|
86
|
+
</Notice>
|
|
87
|
+
) : catalog.data.items.length === 0 ? (
|
|
88
|
+
<Notice
|
|
89
|
+
icon={<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
90
|
+
title={i18n.t("tools.empty")}
|
|
91
|
+
help={i18n.t("tools.emptyHelp")}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
74
95
|
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
75
96
|
<ul className="divide-y">
|
|
76
97
|
{catalog.data.items.map((capability) => (
|
|
77
98
|
<li key={capability.name}>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
)}
|
|
102
|
-
</button>
|
|
99
|
+
<ToolEntry
|
|
100
|
+
capability={capability}
|
|
101
|
+
i18n={i18n}
|
|
102
|
+
open={capability.name === requested}
|
|
103
|
+
/>
|
|
103
104
|
</li>
|
|
104
105
|
))}
|
|
105
106
|
</ul>
|
|
106
107
|
</section>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
<p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
110
111
|
</div>
|
|
111
112
|
</div>
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
function Notice({
|
|
117
|
+
icon,
|
|
118
|
+
title,
|
|
119
|
+
help,
|
|
120
|
+
alert,
|
|
121
|
+
children,
|
|
122
|
+
}: {
|
|
123
|
+
icon: React.ReactNode;
|
|
124
|
+
title: string;
|
|
125
|
+
help: string;
|
|
126
|
+
alert?: boolean;
|
|
127
|
+
children?: React.ReactNode;
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<section
|
|
131
|
+
{...(alert ? { role: "alert" } : {})}
|
|
132
|
+
className="rounded-xl border border-dashed bg-card p-12 text-center"
|
|
133
|
+
>
|
|
134
|
+
{icon}
|
|
135
|
+
<h2 className="mt-4 font-semibold">{title}</h2>
|
|
136
|
+
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">{help}</p>
|
|
137
|
+
{children ? <div className="mt-5">{children}</div> : null}
|
|
138
|
+
</section>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// One tool, one disclosure. The row is the only control in the list: it opens the schema and does
|
|
143
|
+
// nothing else — enabling, sharing or permitting a tool happens in the portal, never here.
|
|
144
|
+
function ToolEntry({
|
|
145
|
+
capability,
|
|
146
|
+
i18n,
|
|
147
|
+
open,
|
|
148
|
+
}: {
|
|
149
|
+
capability: ToolCapability;
|
|
150
|
+
i18n: I18n;
|
|
151
|
+
open: boolean;
|
|
152
|
+
}) {
|
|
153
|
+
const origin = toolOrigin(capability.name);
|
|
136
154
|
|
|
137
155
|
return (
|
|
138
|
-
<
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
onChange={(event) => setArgumentsText(event.target.value)}
|
|
165
|
-
rows={9}
|
|
166
|
-
spellCheck={false}
|
|
167
|
-
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"
|
|
168
|
-
/>
|
|
169
|
-
</label>
|
|
170
|
-
{parseError && <p className="text-xs text-destructive">{i18n.t("tools.invalidJson")}</p>}
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
disabled={test.isPending || !safeToTest}
|
|
174
|
-
onClick={() => test.mutate()}
|
|
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"
|
|
176
|
-
>
|
|
177
|
-
<Play aria-hidden="true" className="size-4" />
|
|
178
|
-
{i18n.t("tools.runTest")}
|
|
179
|
-
</button>
|
|
180
|
-
{test.data && (
|
|
181
|
-
<pre className="max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
182
|
-
{JSON.stringify(test.data, null, 2)}
|
|
183
|
-
</pre>
|
|
156
|
+
<Collapsible defaultOpen={open}>
|
|
157
|
+
<CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
|
158
|
+
<ChevronRight
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
161
|
+
/>
|
|
162
|
+
<span className="min-w-0 flex-1">
|
|
163
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
164
|
+
<span className="text-sm font-medium">{capability.title ?? capability.name}</span>
|
|
165
|
+
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
166
|
+
<span className="sr-only">{i18n.t("tools.origin")}: </span>
|
|
167
|
+
{origin ?? i18n.t("tools.originPortal")}
|
|
168
|
+
</span>
|
|
169
|
+
{capability.annotations.destructiveHint && (
|
|
170
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
171
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
172
|
+
{i18n.t("tools.destructiveShort")}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</span>
|
|
176
|
+
{/* The namespaced name is what a flow references, so it stays readable — but only where
|
|
177
|
+
it is not already the heading of the row. */}
|
|
178
|
+
{capability.title && (
|
|
179
|
+
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
180
|
+
{capability.name}
|
|
181
|
+
</span>
|
|
184
182
|
)}
|
|
185
|
-
{
|
|
186
|
-
<
|
|
183
|
+
{capability.description && (
|
|
184
|
+
<span className="mt-2 block text-xs text-muted-foreground">
|
|
185
|
+
{capability.description}
|
|
186
|
+
</span>
|
|
187
187
|
)}
|
|
188
|
-
</
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
</span>
|
|
189
|
+
</CollapsibleTrigger>
|
|
190
|
+
<CollapsibleContent className="space-y-3 px-5 pb-4 pl-13">
|
|
191
|
+
<Schema label={i18n.t("tools.inputSchema")} value={capability.inputSchema} />
|
|
192
|
+
{capability.outputSchema && (
|
|
193
|
+
<Schema label={i18n.t("tools.outputSchema")} value={capability.outputSchema} />
|
|
194
|
+
)}
|
|
195
|
+
</CollapsibleContent>
|
|
196
|
+
</Collapsible>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function Schema({ label, value }: { label: string; value: Record<string, unknown> }) {
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<p className="text-xs font-medium">{label}</p>
|
|
204
|
+
<pre className="mt-1 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
205
|
+
{JSON.stringify(value, null, 2)}
|
|
206
|
+
</pre>
|
|
207
|
+
</div>
|
|
191
208
|
);
|
|
192
209
|
}
|