@anchrd/intel-ui 0.4.0 → 0.6.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/package.json +1 -1
- package/src/app/action-slot/action-slot.tsx +27 -0
- package/src/app/app-sidebar/app-sidebar.tsx +39 -24
- package/src/app/app-tree/app-tree.tsx +332 -60
- package/src/app/app.tsx +31 -5
- package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +2 -1
- package/src/app/tree-move/tree-move.tsx +331 -0
- package/src/app/user-footer/user-footer.tsx +73 -46
- 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/dropdown-menu.tsx +78 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +135 -57
- package/src/data/intel-data-provider/intel-data-provider.types.ts +51 -13
- package/src/document-link/document-link.tsx +132 -0
- package/src/flow-runs/flow-runs.tsx +225 -0
- package/src/flows/flows.tsx +665 -271
- package/src/flows/node-icon/node-icon.ts +28 -0
- package/src/flows/node-palette/node-palette.tsx +200 -0
- package/src/flows/node-palette/node-palette.types.ts +15 -0
- package/src/graph-pane/graph-pane.tsx +44 -0
- package/src/i18n/en.json +144 -29
- package/src/knowledge/knowledge.tsx +91 -367
- 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 +141 -0
- package/src/main.tsx +2 -2
- package/src/resource-menu/resource-menu.tsx +615 -0
- package/src/router/selection-search.ts +27 -3
- package/src/save-button/save-button.tsx +103 -0
- package/src/styles.css +37 -0
- package/src/theme/theme.ts +24 -0
- package/src/title-row/title-row.tsx +49 -0
- package/src/tools/tools.tsx +57 -38
- package/src/app/header-actions/header-actions.tsx +0 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
2
|
<title>Intel</title>
|
|
3
|
-
<rect width="32" height="32" rx="8" fill="oklch(0.
|
|
4
|
-
<path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.
|
|
3
|
+
<rect width="32" height="32" rx="8" fill="oklch(0.205 0 0)"/>
|
|
4
|
+
<path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.985 0 0)" stroke-width="2" stroke-linecap="round"/>
|
|
5
5
|
</svg>
|
package/src/branding/favicon.svg
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
2
|
<title>Intel</title>
|
|
3
|
-
<rect width="32" height="32" rx="8" fill="oklch(0.
|
|
4
|
-
<path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.
|
|
3
|
+
<rect width="32" height="32" rx="8" fill="oklch(0.205 0 0)"/>
|
|
4
|
+
<path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.985 0 0)" stroke-width="2" stroke-linecap="round"/>
|
|
5
5
|
</svg>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Check, ChevronRight } from "lucide-react";
|
|
1
2
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
2
3
|
import type * as React from "react";
|
|
3
4
|
import { cn } from "@/lib/utils";
|
|
@@ -61,6 +62,78 @@ function DropdownMenuItem({
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
66
|
+
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function DropdownMenuSubTrigger({
|
|
70
|
+
className,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>) {
|
|
74
|
+
return (
|
|
75
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
76
|
+
data-slot="dropdown-menu-sub-trigger"
|
|
77
|
+
className={cn(
|
|
78
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
<ChevronRight aria-hidden="true" className="ml-auto" />
|
|
85
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function DropdownMenuSubContent({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
93
|
+
return (
|
|
94
|
+
<DropdownMenuPrimitive.Portal>
|
|
95
|
+
<DropdownMenuPrimitive.SubContent
|
|
96
|
+
data-slot="dropdown-menu-sub-content"
|
|
97
|
+
className={cn(
|
|
98
|
+
"z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
</DropdownMenuPrimitive.Portal>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function DropdownMenuRadioGroup({
|
|
108
|
+
...props
|
|
109
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
110
|
+
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function DropdownMenuRadioItem({
|
|
114
|
+
className,
|
|
115
|
+
children,
|
|
116
|
+
...props
|
|
117
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
118
|
+
return (
|
|
119
|
+
<DropdownMenuPrimitive.RadioItem
|
|
120
|
+
data-slot="dropdown-menu-radio-item"
|
|
121
|
+
className={cn(
|
|
122
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
|
123
|
+
className,
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
128
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
129
|
+
<Check aria-hidden="true" />
|
|
130
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
131
|
+
</span>
|
|
132
|
+
{children}
|
|
133
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
function DropdownMenuSeparator({
|
|
65
138
|
className,
|
|
66
139
|
...props
|
|
@@ -79,6 +152,11 @@ export {
|
|
|
79
152
|
DropdownMenuContent,
|
|
80
153
|
DropdownMenuItem,
|
|
81
154
|
DropdownMenuLabel,
|
|
155
|
+
DropdownMenuRadioGroup,
|
|
156
|
+
DropdownMenuRadioItem,
|
|
82
157
|
DropdownMenuSeparator,
|
|
158
|
+
DropdownMenuSub,
|
|
159
|
+
DropdownMenuSubContent,
|
|
160
|
+
DropdownMenuSubTrigger,
|
|
83
161
|
DropdownMenuTrigger,
|
|
84
162
|
};
|
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AppendKnowledgeTableRowsInput,
|
|
3
|
+
AppendKnowledgeTableRowsResult,
|
|
2
4
|
ArchiveKnowledgeNodeInput,
|
|
3
5
|
CompleteFlowRunStepInput,
|
|
4
6
|
CreateFlowInput,
|
|
5
|
-
CreateKnowledgeLinkInput,
|
|
6
7
|
CreateKnowledgeNodeInput,
|
|
7
|
-
|
|
8
|
+
DefineKnowledgeTableInput,
|
|
8
9
|
Flow,
|
|
9
10
|
FlowDocument,
|
|
10
11
|
FlowList,
|
|
12
|
+
FlowPublishPreview,
|
|
13
|
+
FlowRequirements,
|
|
14
|
+
FlowRunHistory,
|
|
15
|
+
FlowRunList,
|
|
11
16
|
FlowRunStep,
|
|
12
17
|
KnowledgeDocument,
|
|
13
18
|
KnowledgeGraph,
|
|
14
|
-
KnowledgeLink,
|
|
15
19
|
KnowledgeLinkList,
|
|
16
20
|
KnowledgeNode,
|
|
17
21
|
KnowledgeNodeList,
|
|
22
|
+
KnowledgeTable,
|
|
18
23
|
KnowledgeVersionList,
|
|
24
|
+
ListFlowRunsInput,
|
|
19
25
|
ListFlowsInput,
|
|
20
26
|
ListKnowledgeNodesInput,
|
|
27
|
+
PreviewFlowPublishInput,
|
|
21
28
|
ProblemDetails,
|
|
22
29
|
PublishFlowInput,
|
|
23
|
-
|
|
30
|
+
RelationGraph,
|
|
31
|
+
RelationGraphInput,
|
|
32
|
+
ResolveKnowledgeLinksInput,
|
|
33
|
+
ResolveKnowledgeLinksResult,
|
|
24
34
|
ResourceGrantList,
|
|
25
|
-
RevokeFlowGrantInput,
|
|
26
35
|
RevokeGrantResult,
|
|
27
36
|
RevokeKnowledgeGrantInput,
|
|
28
37
|
SaveFlowVersionInput,
|
|
@@ -31,17 +40,30 @@ import {
|
|
|
31
40
|
SearchKnowledgeInput,
|
|
32
41
|
SearchKnowledgeResult,
|
|
33
42
|
SessionUser,
|
|
34
|
-
ShareFlowInput,
|
|
35
43
|
ShareKnowledgeInput,
|
|
44
|
+
ShareKnowledgeResult,
|
|
36
45
|
StartFlowRunInput,
|
|
37
46
|
ToolCatalog,
|
|
38
47
|
UpdateFlowInput,
|
|
39
48
|
UpdateKnowledgeNodeInput,
|
|
40
49
|
} from "@anchrd/intel-contract";
|
|
41
|
-
import { z } from "zod";
|
|
50
|
+
import type { z } from "zod";
|
|
42
51
|
import type { IntelDataProvider, TreeEntry } from "./intel-data-provider.types.ts";
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
// ⚠️ The refusal's `code`, not only its prose. A view that has to tell four different refusals
|
|
54
|
+
// apart — "you may not write there", "that is not a folder", "that would be a cycle", "someone
|
|
55
|
+
// else changed it" — cannot do so from `detail`, which is a sentence the server is free to
|
|
56
|
+
// reword. The code is the contract (`ProblemDetails.code`); the message stays what it was.
|
|
57
|
+
export class IntelRequestError extends Error {
|
|
58
|
+
constructor(
|
|
59
|
+
public readonly status: number,
|
|
60
|
+
public readonly code: string | null,
|
|
61
|
+
message: string,
|
|
62
|
+
) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "IntelRequestError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
45
67
|
|
|
46
68
|
// Attachment uploads and downloads share this budget, so it is generous rather than snappy.
|
|
47
69
|
const RequestTimeoutMs = 60_000;
|
|
@@ -51,11 +73,31 @@ export function loginPath(returnTo?: string): string {
|
|
|
51
73
|
return returnTo ? `/auth/login?returnTo=${encodeURIComponent(returnTo)}` : "/auth/login";
|
|
52
74
|
}
|
|
53
75
|
|
|
76
|
+
/**
|
|
77
|
+
* One flow as one row of the tree. Two levels show flows — the contents of a folder, and what an
|
|
78
|
+
* expanded flow calls — and they are two lists of the same rows, so they are built here once. Two
|
|
79
|
+
* copies of this would keep returning something plausible and slowly stop agreeing on what a flow
|
|
80
|
+
* row is (#30).
|
|
81
|
+
*/
|
|
82
|
+
export function flowEntry(flow: Flow): TreeEntry {
|
|
83
|
+
return { type: "flow", id: flow.id, title: flow.title, kind: "flow", flow };
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
export function createIntelDataProvider(
|
|
55
|
-
deps: {
|
|
87
|
+
deps: {
|
|
88
|
+
fetch?: typeof fetch;
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
onUnauthorized?(): void;
|
|
91
|
+
navigate?(url: string): void;
|
|
92
|
+
} = {},
|
|
56
93
|
): IntelDataProvider {
|
|
57
94
|
const doFetch = deps.fetch ?? fetch;
|
|
58
95
|
const baseUrl = deps.baseUrl?.replace(/\/$/, "") ?? "";
|
|
96
|
+
const navigate =
|
|
97
|
+
deps.navigate ??
|
|
98
|
+
((url: string) => {
|
|
99
|
+
if (typeof window !== "undefined") window.location.assign(url);
|
|
100
|
+
});
|
|
59
101
|
const unauthorized =
|
|
60
102
|
deps.onUnauthorized ??
|
|
61
103
|
(() => {
|
|
@@ -86,7 +128,9 @@ export function createIntelDataProvider(
|
|
|
86
128
|
.json()
|
|
87
129
|
.catch(() => null),
|
|
88
130
|
);
|
|
89
|
-
throw new
|
|
131
|
+
throw new IntelRequestError(
|
|
132
|
+
result.status,
|
|
133
|
+
parsed.success ? (parsed.data.code ?? null) : null,
|
|
90
134
|
parsed.success
|
|
91
135
|
? (parsed.data.detail ?? parsed.data.title)
|
|
92
136
|
: `Intel responded with ${result.status}`,
|
|
@@ -144,13 +188,7 @@ export function createIntelDataProvider(
|
|
|
144
188
|
kind: node.kind,
|
|
145
189
|
node,
|
|
146
190
|
})),
|
|
147
|
-
...flows.items.map(
|
|
148
|
-
type: "flow" as const,
|
|
149
|
-
id: flow.id,
|
|
150
|
-
title: flow.title,
|
|
151
|
-
kind: "flow" as const,
|
|
152
|
-
flow,
|
|
153
|
-
})),
|
|
191
|
+
...flows.items.map(flowEntry),
|
|
154
192
|
];
|
|
155
193
|
// Folders first, then everything else by title: a document and a flow sit side by side, and
|
|
156
194
|
// the icon is what tells them apart.
|
|
@@ -176,21 +214,13 @@ export function createIntelDataProvider(
|
|
|
176
214
|
async listKnowledgeLinks(nodeId) {
|
|
177
215
|
return await request(`/knowledge/${encodeURIComponent(nodeId)}/links`, KnowledgeLinkList);
|
|
178
216
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
async deleteKnowledgeLink(input) {
|
|
188
|
-
const parsed = DeleteKnowledgeLinkInput.parse(input);
|
|
189
|
-
return await request(
|
|
190
|
-
`/knowledge/${encodeURIComponent(parsed.sourceNodeId)}/links/${encodeURIComponent(parsed.linkId)}/revoke`,
|
|
191
|
-
DeleteResult,
|
|
192
|
-
{ method: "POST", body: JSON.stringify(parsed) },
|
|
193
|
-
);
|
|
217
|
+
// ⚠️ POST, not GET. The identifiers of the documents a text links to belong in a body: a
|
|
218
|
+
// query string of them would end up in logs and referrers.
|
|
219
|
+
async resolveKnowledgeLinks(input) {
|
|
220
|
+
return await request("/knowledge/links/resolve", ResolveKnowledgeLinksResult, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
body: JSON.stringify(ResolveKnowledgeLinksInput.parse(input)),
|
|
223
|
+
});
|
|
194
224
|
},
|
|
195
225
|
async saveKnowledge(input) {
|
|
196
226
|
const parsed = SaveKnowledgeVersionInput.parse(input);
|
|
@@ -211,6 +241,28 @@ export function createIntelDataProvider(
|
|
|
211
241
|
async getKnowledgeAttachment(nodeId) {
|
|
212
242
|
return await (await response(`/knowledge/${encodeURIComponent(nodeId)}/attachment`)).blob();
|
|
213
243
|
},
|
|
244
|
+
async getKnowledgeTable(nodeId) {
|
|
245
|
+
return await request(`/knowledge/${encodeURIComponent(nodeId)}/table`, KnowledgeTable);
|
|
246
|
+
},
|
|
247
|
+
async defineKnowledgeTable(input) {
|
|
248
|
+
const parsed = DefineKnowledgeTableInput.parse(input);
|
|
249
|
+
return await request(
|
|
250
|
+
`/knowledge/${encodeURIComponent(parsed.nodeId)}/table`,
|
|
251
|
+
KnowledgeTable,
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
body: JSON.stringify(parsed),
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
async appendKnowledgeTableRows(input) {
|
|
259
|
+
const parsed = AppendKnowledgeTableRowsInput.parse(input);
|
|
260
|
+
return await request(
|
|
261
|
+
`/knowledge/${encodeURIComponent(parsed.nodeId)}/table/rows`,
|
|
262
|
+
AppendKnowledgeTableRowsResult,
|
|
263
|
+
{ method: "POST", body: JSON.stringify(parsed) },
|
|
264
|
+
);
|
|
265
|
+
},
|
|
214
266
|
async listKnowledgeVersions(nodeId) {
|
|
215
267
|
return await request(
|
|
216
268
|
`/knowledge/${encodeURIComponent(nodeId)}/versions`,
|
|
@@ -248,7 +300,7 @@ export function createIntelDataProvider(
|
|
|
248
300
|
const parsed = ShareKnowledgeInput.parse(input);
|
|
249
301
|
return await request(
|
|
250
302
|
`/knowledge/${encodeURIComponent(parsed.resourceId)}/grants`,
|
|
251
|
-
|
|
303
|
+
ShareKnowledgeResult,
|
|
252
304
|
{ method: "POST", body: JSON.stringify(parsed) },
|
|
253
305
|
);
|
|
254
306
|
},
|
|
@@ -264,6 +316,30 @@ export function createIntelDataProvider(
|
|
|
264
316
|
async getFlow(flowId) {
|
|
265
317
|
return await request(`/flows/${encodeURIComponent(flowId)}`, FlowDocument);
|
|
266
318
|
},
|
|
319
|
+
async listFlowCalls(flowId) {
|
|
320
|
+
return await request(`/flows/${encodeURIComponent(flowId)}/calls`, FlowList);
|
|
321
|
+
},
|
|
322
|
+
async getRelationGraph(scope, limit) {
|
|
323
|
+
const parsed = RelationGraphInput.parse({
|
|
324
|
+
scope,
|
|
325
|
+
...(limit === undefined ? {} : { limit }),
|
|
326
|
+
});
|
|
327
|
+
const params = new URLSearchParams({
|
|
328
|
+
of: parsed.scope.of,
|
|
329
|
+
limit: String(parsed.limit),
|
|
330
|
+
// A folder scope without an ID is the root of the shared tree, and an absent parameter is
|
|
331
|
+
// how that is said — the same distinction `/flows?parentId=` makes.
|
|
332
|
+
...(parsed.scope.of === "flow"
|
|
333
|
+
? { flowId: parsed.scope.flowId }
|
|
334
|
+
: parsed.scope.folderId
|
|
335
|
+
? { folderId: parsed.scope.folderId }
|
|
336
|
+
: {}),
|
|
337
|
+
});
|
|
338
|
+
return await request(`/flows/graph?${params}`, RelationGraph);
|
|
339
|
+
},
|
|
340
|
+
async getFlowRequirements(flowId) {
|
|
341
|
+
return await request(`/flows/${encodeURIComponent(flowId)}/requirements`, FlowRequirements);
|
|
342
|
+
},
|
|
267
343
|
async createFlow(input) {
|
|
268
344
|
return await request("/flows", Flow, {
|
|
269
345
|
method: "POST",
|
|
@@ -277,28 +353,6 @@ export function createIntelDataProvider(
|
|
|
277
353
|
body: JSON.stringify(parsed),
|
|
278
354
|
});
|
|
279
355
|
},
|
|
280
|
-
async listFlowGrants(flowId) {
|
|
281
|
-
return await request(`/flows/${encodeURIComponent(flowId)}/grants`, ResourceGrantList);
|
|
282
|
-
},
|
|
283
|
-
async shareFlow(input) {
|
|
284
|
-
const parsed = ShareFlowInput.parse(input);
|
|
285
|
-
return await request(
|
|
286
|
-
`/flows/${encodeURIComponent(parsed.resourceId)}/grants`,
|
|
287
|
-
ResourceGrant,
|
|
288
|
-
{
|
|
289
|
-
method: "POST",
|
|
290
|
-
body: JSON.stringify(parsed),
|
|
291
|
-
},
|
|
292
|
-
);
|
|
293
|
-
},
|
|
294
|
-
async revokeFlowGrant(input) {
|
|
295
|
-
const parsed = RevokeFlowGrantInput.parse(input);
|
|
296
|
-
return await request(
|
|
297
|
-
`/flows/${encodeURIComponent(parsed.resourceId)}/grants/${encodeURIComponent(parsed.grantId)}/revoke`,
|
|
298
|
-
RevokeGrantResult,
|
|
299
|
-
{ method: "POST", body: JSON.stringify(parsed) },
|
|
300
|
-
);
|
|
301
|
-
},
|
|
302
356
|
async saveFlow(input) {
|
|
303
357
|
const parsed = SaveFlowVersionInput.parse(input);
|
|
304
358
|
return await request(`/flows/${encodeURIComponent(parsed.flowId)}/versions`, FlowDocument, {
|
|
@@ -306,6 +360,13 @@ export function createIntelDataProvider(
|
|
|
306
360
|
body: JSON.stringify(parsed),
|
|
307
361
|
});
|
|
308
362
|
},
|
|
363
|
+
async previewFlowPublish(input) {
|
|
364
|
+
const parsed = PreviewFlowPublishInput.parse(input);
|
|
365
|
+
return await request(
|
|
366
|
+
`/flows/${encodeURIComponent(parsed.flowId)}/versions/${encodeURIComponent(parsed.versionId)}/publish-preview`,
|
|
367
|
+
FlowPublishPreview,
|
|
368
|
+
);
|
|
369
|
+
},
|
|
309
370
|
async publishFlow(input) {
|
|
310
371
|
const parsed = PublishFlowInput.parse(input);
|
|
311
372
|
return await request(`/flows/${encodeURIComponent(parsed.flowId)}/publish`, Flow, {
|
|
@@ -320,9 +381,24 @@ export function createIntelDataProvider(
|
|
|
320
381
|
body: JSON.stringify(parsed),
|
|
321
382
|
});
|
|
322
383
|
},
|
|
384
|
+
async listFlowRuns(input) {
|
|
385
|
+
const parsed = ListFlowRunsInput.parse(input);
|
|
386
|
+
// Only what was actually asked for travels. `failedOnly` is the parameter's presence rather
|
|
387
|
+
// than a value, which is the same shape the server reads it back with.
|
|
388
|
+
const params = new URLSearchParams({ limit: String(parsed.limit) });
|
|
389
|
+
if (parsed.failedOnly) params.set("failedOnly", "");
|
|
390
|
+
if (parsed.cursor) params.set("cursor", parsed.cursor);
|
|
391
|
+
return await request(
|
|
392
|
+
`/flows/${encodeURIComponent(parsed.flowId)}/runs?${params}`,
|
|
393
|
+
FlowRunList,
|
|
394
|
+
);
|
|
395
|
+
},
|
|
323
396
|
async getFlowRun(runId) {
|
|
324
397
|
return await request(`/flow-runs/${encodeURIComponent(runId)}`, FlowRunStep);
|
|
325
398
|
},
|
|
399
|
+
async getFlowRunSteps(runId) {
|
|
400
|
+
return await request(`/flow-runs/${encodeURIComponent(runId)}/steps`, FlowRunHistory);
|
|
401
|
+
},
|
|
326
402
|
async completeFlowStep(input) {
|
|
327
403
|
const parsed = CompleteFlowRunStepInput.parse(input);
|
|
328
404
|
return await request(`/flow-runs/${encodeURIComponent(parsed.runId)}/complete`, FlowRunStep, {
|
|
@@ -334,8 +410,10 @@ export function createIntelDataProvider(
|
|
|
334
410
|
return await request("/tools", ToolCatalog);
|
|
335
411
|
},
|
|
336
412
|
|
|
337
|
-
|
|
338
|
-
|
|
413
|
+
// The Worker serves the connect route, so this navigation belongs to the data layer for the
|
|
414
|
+
// same reason `loginPath` does. `silent=1` is what makes it a redirect nobody has to watch.
|
|
415
|
+
startPortalSignIn(returnTo = "/tools") {
|
|
416
|
+
navigate(`${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}&silent=1`);
|
|
339
417
|
},
|
|
340
418
|
async logout() {
|
|
341
419
|
const response = await doFetch(`${baseUrl}/auth/logout`, {
|
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AppendKnowledgeTableRowsInput,
|
|
3
|
+
AppendKnowledgeTableRowsResult,
|
|
2
4
|
ArchiveKnowledgeNodeInput,
|
|
3
5
|
CompleteFlowRunStepInput,
|
|
4
6
|
CreateFlowInput,
|
|
5
|
-
CreateKnowledgeLinkInput,
|
|
6
7
|
CreateKnowledgeNodeInput,
|
|
7
|
-
|
|
8
|
+
DefineKnowledgeTableInput,
|
|
8
9
|
Flow,
|
|
9
10
|
FlowDocument,
|
|
10
11
|
FlowList,
|
|
12
|
+
FlowPublishPreview,
|
|
13
|
+
FlowRequirements,
|
|
14
|
+
FlowRunHistory,
|
|
15
|
+
FlowRunList,
|
|
11
16
|
FlowRunStep,
|
|
12
17
|
KnowledgeDocument,
|
|
13
18
|
KnowledgeGraph,
|
|
14
|
-
KnowledgeLink,
|
|
15
19
|
KnowledgeLinkList,
|
|
16
20
|
KnowledgeNode,
|
|
17
21
|
KnowledgeNodeKind,
|
|
18
22
|
KnowledgeNodeList,
|
|
23
|
+
KnowledgeTable,
|
|
19
24
|
KnowledgeVersionList,
|
|
25
|
+
ListFlowRunsInput,
|
|
20
26
|
ListFlowsInput,
|
|
21
27
|
ListKnowledgeNodesInput,
|
|
28
|
+
PreviewFlowPublishInput,
|
|
22
29
|
PublishFlowInput,
|
|
23
|
-
|
|
30
|
+
RelationGraph,
|
|
31
|
+
RelationGraphScope,
|
|
32
|
+
ResolveKnowledgeLinksInput,
|
|
33
|
+
ResolveKnowledgeLinksResult,
|
|
24
34
|
ResourceGrantList,
|
|
25
|
-
RevokeFlowGrantInput,
|
|
26
35
|
RevokeGrantResult,
|
|
27
36
|
RevokeKnowledgeGrantInput,
|
|
28
37
|
SaveFlowVersionInput,
|
|
@@ -31,8 +40,8 @@ import type {
|
|
|
31
40
|
SearchKnowledgeInput,
|
|
32
41
|
SearchKnowledgeResult,
|
|
33
42
|
SessionUser,
|
|
34
|
-
ShareFlowInput,
|
|
35
43
|
ShareKnowledgeInput,
|
|
44
|
+
ShareKnowledgeResult,
|
|
36
45
|
StartFlowRunInput,
|
|
37
46
|
ToolCatalog,
|
|
38
47
|
UpdateFlowInput,
|
|
@@ -55,30 +64,59 @@ export interface IntelDataProvider {
|
|
|
55
64
|
createKnowledge(input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
56
65
|
getKnowledgeGraph(limit?: number): Promise<KnowledgeGraph>;
|
|
57
66
|
listKnowledgeLinks(nodeId: string): Promise<KnowledgeLinkList>;
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
// The titles of the documents a text links to, for the reader. A target they may not see, or one
|
|
68
|
+
// that is gone, is absent from the answer — the two look the same on purpose (#41).
|
|
69
|
+
resolveKnowledgeLinks(input: ResolveKnowledgeLinksInput): Promise<ResolveKnowledgeLinksResult>;
|
|
60
70
|
saveKnowledge(input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
|
|
61
71
|
saveKnowledgeAttachment(input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
|
|
62
72
|
getKnowledgeAttachment(nodeId: string): Promise<Blob>;
|
|
63
|
-
|
|
73
|
+
// A table as columns and rows. The CSV stays canonical — `getKnowledge` still answers with it,
|
|
74
|
+
// which is what the download uses — and this is the same bytes read by the server's one reader.
|
|
75
|
+
getKnowledgeTable(nodeId: string): Promise<KnowledgeTable>;
|
|
76
|
+
defineKnowledgeTable(input: DefineKnowledgeTableInput): Promise<KnowledgeTable>;
|
|
77
|
+
appendKnowledgeTableRows(
|
|
78
|
+
input: AppendKnowledgeTableRowsInput,
|
|
79
|
+
): Promise<AppendKnowledgeTableRowsResult>;
|
|
80
|
+
// Walks the browser through the portal's OAuth flow without asking anybody anything: Gate is the
|
|
81
|
+
// identity provider Cloudflare Access consumes, so a signed-in person is already known there
|
|
82
|
+
// (#60). It navigates away — the caller renders no button for it and gets no answer back.
|
|
83
|
+
startPortalSignIn(returnTo?: string): void;
|
|
64
84
|
listKnowledgeVersions(nodeId: string): Promise<KnowledgeVersionList>;
|
|
65
85
|
updateKnowledge(input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
66
86
|
archiveKnowledge(input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
67
87
|
searchKnowledge(input: SearchKnowledgeInput): Promise<SearchKnowledgeResult>;
|
|
68
88
|
listKnowledgeGrants(resourceId: string): Promise<ResourceGrantList>;
|
|
69
|
-
|
|
89
|
+
// The grant, and what the grant does not cover: the documents the flows in this folder read that
|
|
90
|
+
// the new principal still cannot. A warning, never a refusal (ADR-0004 §4).
|
|
91
|
+
shareKnowledge(input: ShareKnowledgeInput): Promise<ShareKnowledgeResult>;
|
|
70
92
|
revokeKnowledgeGrant(input: RevokeKnowledgeGrantInput): Promise<RevokeGrantResult>;
|
|
71
93
|
listFlows(input?: ListFlowsInput): Promise<FlowList>;
|
|
72
94
|
getFlow(flowId: string): Promise<FlowDocument>;
|
|
95
|
+
// What a flow calls, read out of its graph. It answers a different question from `listTreeChildren`
|
|
96
|
+
// and deliberately gives a different answer: a shared sub-flow is listed under every caller.
|
|
97
|
+
listFlowCalls(flowId: string): Promise<FlowList>;
|
|
98
|
+
// What accesses what, for one level of the shared tree: a folder for its contents, a flow for
|
|
99
|
+
// itself. ⚠️ Only nodes the signed-in user may see come back, and one they may not is absent
|
|
100
|
+
// altogether — never a placeholder, because the edge into one would already say that it exists.
|
|
101
|
+
getRelationGraph(scope: RelationGraphScope, limit?: number): Promise<RelationGraph>;
|
|
102
|
+
// What a flow touches: the documents and tools its graph names. Documents the signed-in user may
|
|
103
|
+
// not see are counted rather than named, and nothing here claims anybody may reach them.
|
|
104
|
+
getFlowRequirements(flowId: string): Promise<FlowRequirements>;
|
|
73
105
|
createFlow(input: CreateFlowInput): Promise<Flow>;
|
|
74
106
|
updateFlow(input: UpdateFlowInput): Promise<Flow>;
|
|
75
|
-
listFlowGrants(flowId: string): Promise<ResourceGrantList>;
|
|
76
|
-
shareFlow(input: ShareFlowInput): Promise<ResourceGrant>;
|
|
77
|
-
revokeFlowGrant(input: RevokeFlowGrantInput): Promise<RevokeGrantResult>;
|
|
78
107
|
saveFlow(input: SaveFlowVersionInput): Promise<FlowDocument>;
|
|
108
|
+
// Which version each sub-flow call will take once published, and which of them publishing
|
|
109
|
+
// freezes. Read before publishing, so the author agrees to the pins rather than discovering them.
|
|
110
|
+
previewFlowPublish(input: PreviewFlowPublishInput): Promise<FlowPublishPreview>;
|
|
79
111
|
publishFlow(input: PublishFlowInput): Promise<Flow>;
|
|
80
112
|
startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
|
|
113
|
+
// What this flow has done, newest first, one page at a time. ⚠️ It carries what a run did and
|
|
114
|
+
// never what it produced: a run reads Knowledge with the rights of whoever started it, so its
|
|
115
|
+
// result is not automatically readable for everyone who may read the flow.
|
|
116
|
+
listFlowRuns(input: ListFlowRunsInput): Promise<FlowRunList>;
|
|
81
117
|
getFlowRun(runId: string): Promise<FlowRunStep>;
|
|
118
|
+
// The drill-down behind one run: every step it took, and the call chain it belongs to.
|
|
119
|
+
getFlowRunSteps(runId: string): Promise<FlowRunHistory>;
|
|
82
120
|
completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
|
|
83
121
|
// Reading the catalog is the whole of the tool surface here: calling a tool belongs to a flow
|
|
84
122
|
// or to the Intel MCP surface, not to the screen that shows what the portal offers.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DocumentLinkInlineType } from "@anchrd/intel-contract";
|
|
2
|
+
import { BlockNoteSchema } from "@blocknote/core";
|
|
3
|
+
import { createReactInlineContentSpec } from "@blocknote/react";
|
|
4
|
+
import { FileText, Link2Off } from "lucide-react";
|
|
5
|
+
import { createContext, useContext } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What a document link resolves to for the person reading it.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ `titles` holds only the documents this reader may see. A missing entry is the whole answer for
|
|
11
|
+
* both of the reasons an entry can be missing — the target is gone, or it is not theirs to reach —
|
|
12
|
+
* and nothing here may ever be given a way to tell those apart. `pending` exists so the editor can
|
|
13
|
+
* hold back the difference between "not resolved yet" and "does not resolve", which would otherwise
|
|
14
|
+
* flash a broken link at every reader for one render.
|
|
15
|
+
*/
|
|
16
|
+
export interface DocumentLinkResolution {
|
|
17
|
+
titles: ReadonlyMap<string, string>;
|
|
18
|
+
pending: boolean;
|
|
19
|
+
// The one word an unresolved link says. It travels in the context because a BlockNote inline
|
|
20
|
+
// element is rendered by the editor rather than by a view that could reach the i18n catalog.
|
|
21
|
+
unresolvedLabel: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DocumentLinkContext = createContext<DocumentLinkResolution>({
|
|
25
|
+
titles: new Map(),
|
|
26
|
+
pending: false,
|
|
27
|
+
unresolvedLabel: "Unavailable document",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const DocumentLinkProvider = DocumentLinkContext.Provider;
|
|
31
|
+
|
|
32
|
+
export function useDocumentLinkResolution(): DocumentLinkResolution {
|
|
33
|
+
return useContext(DocumentLinkContext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One document link inside the text (#41).
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ The element stores the target's ID and nothing else. A stored title would be wrong the moment
|
|
40
|
+
* the target is renamed and a stored path the moment it is moved — and both would put a name into a
|
|
41
|
+
* document that any reader of it could then read, whether or not they may reach the target.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ An unresolved link says that it does not resolve and nothing more: no title, no path, no ID on
|
|
44
|
+
* screen, no hint of which of the two reasons applies. That is the whole of the leak this feature
|
|
45
|
+
* could have introduced, and the reason the broken form carries no data at all.
|
|
46
|
+
*/
|
|
47
|
+
export function DocumentLinkText({ nodeId }: { nodeId: string }) {
|
|
48
|
+
const { titles, pending, unresolvedLabel } = useDocumentLinkResolution();
|
|
49
|
+
const title = titles.get(nodeId);
|
|
50
|
+
if (title === undefined) {
|
|
51
|
+
return (
|
|
52
|
+
<span
|
|
53
|
+
data-document-link={pending ? "pending" : "unresolved"}
|
|
54
|
+
className={
|
|
55
|
+
pending
|
|
56
|
+
? "inline-flex items-center gap-1 rounded-sm px-1 text-muted-foreground"
|
|
57
|
+
: "inline-flex items-center gap-1 rounded-sm bg-destructive/10 px-1 text-destructive line-through"
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{pending ? null : <Link2Off aria-hidden="true" className="size-3.5" />}
|
|
61
|
+
{pending ? "…" : unresolvedLabel}
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<span
|
|
67
|
+
data-document-link="resolved"
|
|
68
|
+
data-node-id={nodeId}
|
|
69
|
+
className="inline-flex items-center gap-1 rounded-sm bg-muted px-1 text-foreground"
|
|
70
|
+
>
|
|
71
|
+
<FileText aria-hidden="true" className="size-3.5" />
|
|
72
|
+
{title}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const documentLinkSpec = createReactInlineContentSpec(
|
|
78
|
+
{
|
|
79
|
+
type: DocumentLinkInlineType,
|
|
80
|
+
propSchema: { nodeId: { default: "" } },
|
|
81
|
+
// The link has no editable text of its own: what it reads is the target's title, and a title
|
|
82
|
+
// somebody could type over would stop following the target the first time they did (#41).
|
|
83
|
+
content: "none",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
render: (props) => <DocumentLinkText nodeId={String(props.inlineContent.props.nodeId)} />,
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// One schema for the editor and for everything that reads its documents back: BlockNote's defaults
|
|
91
|
+
// plus the one inline element #41 adds.
|
|
92
|
+
//
|
|
93
|
+
// ⚠️ `extend` rather than `create({ inlineContentSpecs: { ...defaults, documentLink } })`. Both
|
|
94
|
+
// build the same schema at run time, but only `extend` keeps the added type in the schema's own
|
|
95
|
+
// generics under this repo's `exactOptionalPropertyTypes` — with the spread form the editor infers
|
|
96
|
+
// BlockNote's default schema, typechecks, and then refuses to insert a document link.
|
|
97
|
+
export const intelEditorSchema = BlockNoteSchema.create().extend({
|
|
98
|
+
inlineContentSpecs: { documentLink: documentLinkSpec },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The schema carries its own editor and block types as declaration-only properties, which is the
|
|
102
|
+
// only way to name them here: writing `BlockNoteEditor<typeof schema.blockSchema, ...>` by hand
|
|
103
|
+
// fails BlockNote's own constraint under `exactOptionalPropertyTypes`.
|
|
104
|
+
export type IntelEditor = (typeof intelEditorSchema)["BlockNoteEditor"];
|
|
105
|
+
export type IntelEditorPartialBlock = (typeof intelEditorSchema)["PartialBlock"];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The IDs a stored BlockNote document links to.
|
|
109
|
+
*
|
|
110
|
+
* The server reads the same thing out of the same content when the document is saved; this side
|
|
111
|
+
* needs it too, to ask which of them it may show a title for.
|
|
112
|
+
*/
|
|
113
|
+
export function documentLinkIds(blocks: unknown): string[] {
|
|
114
|
+
const found = new Set<string>();
|
|
115
|
+
const walk = (value: unknown): void => {
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
for (const entry of value) walk(entry);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (typeof value !== "object" || value === null) return;
|
|
121
|
+
const record = value as Record<string, unknown>;
|
|
122
|
+
if (record.type === DocumentLinkInlineType) {
|
|
123
|
+
const props = record.props as Record<string, unknown> | undefined;
|
|
124
|
+
const nodeId = props?.nodeId;
|
|
125
|
+
if (typeof nodeId === "string" && nodeId.length > 0) found.add(nodeId);
|
|
126
|
+
}
|
|
127
|
+
walk(record.content);
|
|
128
|
+
walk(record.children);
|
|
129
|
+
};
|
|
130
|
+
walk(blocks);
|
|
131
|
+
return [...found];
|
|
132
|
+
}
|