@anchrd/intel-ui 0.2.1 → 0.4.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/app-sidebar/app-sidebar.tsx +56 -0
- package/src/app/app-tree/app-tree.tsx +427 -0
- package/src/app/app.tsx +84 -54
- package/src/app/header-actions/header-actions.tsx +15 -0
- 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 +85 -0
- package/src/app/user-footer/user-footer.tsx +76 -0
- 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 +84 -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 +58 -39
- package/src/data/intel-data-provider/intel-data-provider.types.ts +17 -8
- package/src/flows/flows.tsx +59 -142
- package/src/hooks/use-mobile.ts +19 -0
- package/src/i18n/en.json +48 -29
- package/src/knowledge/knowledge.tsx +133 -423
- package/src/router/router.tsx +4 -0
- package/src/router/selection-search.ts +19 -0
- package/src/styles.css +54 -51
- package/src/tools/tools.tsx +175 -158
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delayDuration = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delayDuration={delayDuration}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
20
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
24
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function TooltipContent({
|
|
28
|
+
className,
|
|
29
|
+
sideOffset = 0,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
33
|
+
return (
|
|
34
|
+
<TooltipPrimitive.Portal>
|
|
35
|
+
<TooltipPrimitive.Content
|
|
36
|
+
data-slot="tooltip-content"
|
|
37
|
+
sideOffset={sideOffset}
|
|
38
|
+
className={cn(
|
|
39
|
+
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
|
46
|
+
</TooltipPrimitive.Content>
|
|
47
|
+
</TooltipPrimitive.Portal>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
KnowledgeNode,
|
|
17
17
|
KnowledgeNodeList,
|
|
18
18
|
KnowledgeVersionList,
|
|
19
|
+
ListFlowsInput,
|
|
19
20
|
ListKnowledgeNodesInput,
|
|
20
21
|
ProblemDetails,
|
|
21
22
|
PublishFlowInput,
|
|
@@ -29,26 +30,22 @@ import {
|
|
|
29
30
|
SaveKnowledgeVersionInput,
|
|
30
31
|
SearchKnowledgeInput,
|
|
31
32
|
SearchKnowledgeResult,
|
|
33
|
+
SessionUser,
|
|
32
34
|
ShareFlowInput,
|
|
33
35
|
ShareKnowledgeInput,
|
|
34
36
|
StartFlowRunInput,
|
|
35
|
-
TestToolInput,
|
|
36
37
|
ToolCatalog,
|
|
37
|
-
|
|
38
|
+
UpdateFlowInput,
|
|
38
39
|
UpdateKnowledgeNodeInput,
|
|
39
40
|
} from "@anchrd/intel-contract";
|
|
40
41
|
import { z } from "zod";
|
|
41
|
-
import type { IntelDataProvider,
|
|
42
|
+
import type { IntelDataProvider, TreeEntry } from "./intel-data-provider.types.ts";
|
|
42
43
|
|
|
43
44
|
const DeleteResult = z.strictObject({ deleted: z.boolean() });
|
|
44
45
|
|
|
45
46
|
// Attachment uploads and downloads share this budget, so it is generous rather than snappy.
|
|
46
47
|
const RequestTimeoutMs = 60_000;
|
|
47
48
|
|
|
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
49
|
// The Worker serves the login route, so the path belongs to the data layer rather than to any view.
|
|
53
50
|
export function loginPath(returnTo?: string): string {
|
|
54
51
|
return returnTo ? `/auth/login?returnTo=${encodeURIComponent(returnTo)}` : "/auth/login";
|
|
@@ -106,6 +103,17 @@ export function createIntelDataProvider(
|
|
|
106
103
|
return schema.parse(await (await response(path, init)).json());
|
|
107
104
|
}
|
|
108
105
|
|
|
106
|
+
async function listFlows(input: ListFlowsInput = {}): Promise<z.infer<typeof FlowList>> {
|
|
107
|
+
const query = ListFlowsInput.parse(input);
|
|
108
|
+
// The parameter is only sent when the caller asked for one folder. Sending an empty one for
|
|
109
|
+
// "everything" would ask the server for the root instead.
|
|
110
|
+
const params = new URLSearchParams(
|
|
111
|
+
query.parentId === undefined ? {} : { parentId: query.parentId ?? "" },
|
|
112
|
+
);
|
|
113
|
+
const search = params.toString();
|
|
114
|
+
return await request(`/flows${search ? `?${search}` : ""}`, FlowList);
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
async function listKnowledge(
|
|
110
118
|
input: Partial<ListKnowledgeNodesInput> = {},
|
|
111
119
|
): Promise<z.infer<typeof KnowledgeNodeList>> {
|
|
@@ -115,31 +123,43 @@ export function createIntelDataProvider(
|
|
|
115
123
|
return await request(`/knowledge?${params}`, KnowledgeNodeList);
|
|
116
124
|
}
|
|
117
125
|
|
|
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 [];
|
|
127
|
-
const { items } = await listKnowledge({ parentId });
|
|
128
|
-
return await Promise.all(
|
|
129
|
-
items.map(async (node) => ({
|
|
130
|
-
...node,
|
|
131
|
-
children:
|
|
132
|
-
node.kind === "folder" && !seen.has(node.id)
|
|
133
|
-
? await loadChildren(node.id, depth + 1, new Set([...seen, node.id]))
|
|
134
|
-
: [],
|
|
135
|
-
})),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
126
|
return {
|
|
127
|
+
async getSession() {
|
|
128
|
+
return await request("/session", SessionUser);
|
|
129
|
+
},
|
|
140
130
|
listKnowledge,
|
|
141
|
-
async
|
|
142
|
-
|
|
131
|
+
async listTreeChildren(parentId) {
|
|
132
|
+
// Both sides of one folder, asked for in parallel and merged here rather than on the server:
|
|
133
|
+
// the folder is shared, the two records are not, and no endpoint may return a row that is a
|
|
134
|
+
// bit of both (ADR-0004).
|
|
135
|
+
const [knowledge, flows] = await Promise.all([
|
|
136
|
+
listKnowledge({ parentId }),
|
|
137
|
+
listFlows({ parentId }),
|
|
138
|
+
]);
|
|
139
|
+
const entries: TreeEntry[] = [
|
|
140
|
+
...knowledge.items.map((node) => ({
|
|
141
|
+
type: "knowledge" as const,
|
|
142
|
+
id: node.id,
|
|
143
|
+
title: node.title,
|
|
144
|
+
kind: node.kind,
|
|
145
|
+
node,
|
|
146
|
+
})),
|
|
147
|
+
...flows.items.map((flow) => ({
|
|
148
|
+
type: "flow" as const,
|
|
149
|
+
id: flow.id,
|
|
150
|
+
title: flow.title,
|
|
151
|
+
kind: "flow" as const,
|
|
152
|
+
flow,
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
// Folders first, then everything else by title: a document and a flow sit side by side, and
|
|
156
|
+
// the icon is what tells them apart.
|
|
157
|
+
return entries.sort(
|
|
158
|
+
(left, right) =>
|
|
159
|
+
Number(right.kind === "folder") - Number(left.kind === "folder") ||
|
|
160
|
+
left.title.localeCompare(right.title) ||
|
|
161
|
+
left.id.localeCompare(right.id),
|
|
162
|
+
);
|
|
143
163
|
},
|
|
144
164
|
async getKnowledge(nodeId) {
|
|
145
165
|
return await request(`/knowledge/${encodeURIComponent(nodeId)}`, KnowledgeDocument);
|
|
@@ -240,9 +260,7 @@ export function createIntelDataProvider(
|
|
|
240
260
|
{ method: "POST", body: JSON.stringify(parsed) },
|
|
241
261
|
);
|
|
242
262
|
},
|
|
243
|
-
|
|
244
|
-
return await request("/flows", FlowList);
|
|
245
|
-
},
|
|
263
|
+
listFlows,
|
|
246
264
|
async getFlow(flowId) {
|
|
247
265
|
return await request(`/flows/${encodeURIComponent(flowId)}`, FlowDocument);
|
|
248
266
|
},
|
|
@@ -252,6 +270,13 @@ export function createIntelDataProvider(
|
|
|
252
270
|
body: JSON.stringify(CreateFlowInput.parse(input)),
|
|
253
271
|
});
|
|
254
272
|
},
|
|
273
|
+
async updateFlow(input) {
|
|
274
|
+
const parsed = UpdateFlowInput.parse(input);
|
|
275
|
+
return await request(`/flows/${encodeURIComponent(parsed.flowId)}`, Flow, {
|
|
276
|
+
method: "PATCH",
|
|
277
|
+
body: JSON.stringify(parsed),
|
|
278
|
+
});
|
|
279
|
+
},
|
|
255
280
|
async listFlowGrants(flowId) {
|
|
256
281
|
return await request(`/flows/${encodeURIComponent(flowId)}/grants`, ResourceGrantList);
|
|
257
282
|
},
|
|
@@ -308,12 +333,6 @@ export function createIntelDataProvider(
|
|
|
308
333
|
async listTools() {
|
|
309
334
|
return await request("/tools", ToolCatalog);
|
|
310
335
|
},
|
|
311
|
-
async testTool(input) {
|
|
312
|
-
return await request("/tools/test", ToolTestResult, {
|
|
313
|
-
method: "POST",
|
|
314
|
-
body: JSON.stringify(TestToolInput.parse(input)),
|
|
315
|
-
});
|
|
316
|
-
},
|
|
317
336
|
|
|
318
337
|
portalConnectUrl(returnTo = "/tools") {
|
|
319
338
|
return `${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}`;
|
|
@@ -14,8 +14,10 @@ import type {
|
|
|
14
14
|
KnowledgeLink,
|
|
15
15
|
KnowledgeLinkList,
|
|
16
16
|
KnowledgeNode,
|
|
17
|
+
KnowledgeNodeKind,
|
|
17
18
|
KnowledgeNodeList,
|
|
18
19
|
KnowledgeVersionList,
|
|
20
|
+
ListFlowsInput,
|
|
19
21
|
ListKnowledgeNodesInput,
|
|
20
22
|
PublishFlowInput,
|
|
21
23
|
ResourceGrant,
|
|
@@ -28,22 +30,27 @@ import type {
|
|
|
28
30
|
SaveKnowledgeVersionInput,
|
|
29
31
|
SearchKnowledgeInput,
|
|
30
32
|
SearchKnowledgeResult,
|
|
33
|
+
SessionUser,
|
|
31
34
|
ShareFlowInput,
|
|
32
35
|
ShareKnowledgeInput,
|
|
33
36
|
StartFlowRunInput,
|
|
34
|
-
TestToolInput,
|
|
35
37
|
ToolCatalog,
|
|
36
|
-
|
|
38
|
+
UpdateFlowInput,
|
|
37
39
|
UpdateKnowledgeNodeInput,
|
|
38
40
|
} from "@anchrd/intel-contract";
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// One row of the shared tree. Knowledge and Flows share the folder, not their nature (ADR-0004), so
|
|
43
|
+
// this is a union that keeps each side's record whole — never a merged "node" that is a bit of both.
|
|
44
|
+
export type TreeEntry =
|
|
45
|
+
| { type: "knowledge"; id: string; title: string; kind: KnowledgeNodeKind; node: KnowledgeNode }
|
|
46
|
+
| { type: "flow"; id: string; title: string; kind: "flow"; flow: Flow };
|
|
43
47
|
|
|
44
48
|
export interface IntelDataProvider {
|
|
49
|
+
getSession(): Promise<SessionUser>;
|
|
45
50
|
listKnowledge(input?: Partial<ListKnowledgeNodesInput>): Promise<KnowledgeNodeList>;
|
|
46
|
-
|
|
51
|
+
// One level of the shared tree: the documents and the flows filed in the same folder, in one
|
|
52
|
+
// sorted list. Per level rather than recursive, so opening a folder is what costs a request.
|
|
53
|
+
listTreeChildren(parentId: string | null): Promise<TreeEntry[]>;
|
|
47
54
|
getKnowledge(nodeId: string): Promise<KnowledgeDocument>;
|
|
48
55
|
createKnowledge(input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
49
56
|
getKnowledgeGraph(limit?: number): Promise<KnowledgeGraph>;
|
|
@@ -61,9 +68,10 @@ export interface IntelDataProvider {
|
|
|
61
68
|
listKnowledgeGrants(resourceId: string): Promise<ResourceGrantList>;
|
|
62
69
|
shareKnowledge(input: ShareKnowledgeInput): Promise<ResourceGrant>;
|
|
63
70
|
revokeKnowledgeGrant(input: RevokeKnowledgeGrantInput): Promise<RevokeGrantResult>;
|
|
64
|
-
listFlows(): Promise<FlowList>;
|
|
71
|
+
listFlows(input?: ListFlowsInput): Promise<FlowList>;
|
|
65
72
|
getFlow(flowId: string): Promise<FlowDocument>;
|
|
66
73
|
createFlow(input: CreateFlowInput): Promise<Flow>;
|
|
74
|
+
updateFlow(input: UpdateFlowInput): Promise<Flow>;
|
|
67
75
|
listFlowGrants(flowId: string): Promise<ResourceGrantList>;
|
|
68
76
|
shareFlow(input: ShareFlowInput): Promise<ResourceGrant>;
|
|
69
77
|
revokeFlowGrant(input: RevokeFlowGrantInput): Promise<RevokeGrantResult>;
|
|
@@ -72,7 +80,8 @@ export interface IntelDataProvider {
|
|
|
72
80
|
startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
|
|
73
81
|
getFlowRun(runId: string): Promise<FlowRunStep>;
|
|
74
82
|
completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
|
|
83
|
+
// Reading the catalog is the whole of the tool surface here: calling a tool belongs to a flow
|
|
84
|
+
// or to the Intel MCP surface, not to the screen that shows what the portal offers.
|
|
75
85
|
listTools(): Promise<ToolCatalog>;
|
|
76
|
-
testTool(input: TestToolInput): Promise<ToolTestResult>;
|
|
77
86
|
logout(): Promise<void>;
|
|
78
87
|
}
|
package/src/flows/flows.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { FlowGraph, FlowNode, ResourceRole } from "@anchrd/intel-contract";
|
|
1
|
+
import type { FlowGraph, FlowNode, KnowledgeNode, ResourceRole } from "@anchrd/intel-contract";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
3
4
|
import {
|
|
4
5
|
addEdge,
|
|
5
6
|
applyEdgeChanges,
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
CirclePlay,
|
|
24
25
|
FileSearch,
|
|
25
26
|
GitBranch,
|
|
26
|
-
Plus,
|
|
27
27
|
Save,
|
|
28
28
|
Send,
|
|
29
29
|
Share2,
|
|
@@ -33,9 +33,10 @@ import {
|
|
|
33
33
|
Wrench,
|
|
34
34
|
} from "lucide-react";
|
|
35
35
|
import { useEffect, useMemo, useState } from "react";
|
|
36
|
-
import
|
|
36
|
+
import { HeaderActions } from "@/app/header-actions/header-actions.tsx";
|
|
37
37
|
import { Modal } from "@/modal/modal.tsx";
|
|
38
38
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
39
|
+
import { selectedFrom } from "@/router/selection-search.ts";
|
|
39
40
|
|
|
40
41
|
type CanvasNode = Node<{ node: FlowNode }, "intel">;
|
|
41
42
|
type CanvasEdge = Edge;
|
|
@@ -229,21 +230,21 @@ function newNode(
|
|
|
229
230
|
export function Flows() {
|
|
230
231
|
const { data, i18n } = useIntelRouterContext();
|
|
231
232
|
const queryClient = useQueryClient();
|
|
232
|
-
const flows = useQuery({
|
|
233
|
-
queryKey: ["flows"],
|
|
234
|
-
queryFn: () => data.listFlows(),
|
|
235
|
-
});
|
|
236
233
|
const tools = useQuery({
|
|
237
234
|
queryKey: ["tools"],
|
|
238
235
|
queryFn: () => data.listTools(),
|
|
239
236
|
});
|
|
237
|
+
// A knowledge step points at nodes, so the editor needs candidates. The graph is the authorized
|
|
238
|
+
// flat list of them; walking the folder tree for a picker would be the N+1 all over again.
|
|
240
239
|
const knowledge = useQuery({
|
|
241
|
-
queryKey: ["knowledge-
|
|
242
|
-
queryFn: () => data.
|
|
240
|
+
queryKey: ["knowledge-graph"],
|
|
241
|
+
queryFn: () => data.getKnowledgeGraph(),
|
|
242
|
+
});
|
|
243
|
+
// The tree in the sidebar and the header search both name the flow to open through `?select=`.
|
|
244
|
+
const selectedFlowId = useRouterState({
|
|
245
|
+
select: (state) => selectedFrom(state.location.search),
|
|
243
246
|
});
|
|
244
|
-
const [selectedFlowId, setSelectedFlowId] = useState<string | null>(null);
|
|
245
247
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
246
|
-
const [creating, setCreating] = useState(false);
|
|
247
248
|
const [sharing, setSharing] = useState(false);
|
|
248
249
|
const document = useQuery({
|
|
249
250
|
queryKey: ["flow", selectedFlowId],
|
|
@@ -274,9 +275,8 @@ export function Flows() {
|
|
|
274
275
|
graph: graph(nodes, edges),
|
|
275
276
|
idempotencyKey: crypto.randomUUID(),
|
|
276
277
|
}),
|
|
277
|
-
onSuccess:
|
|
278
|
+
onSuccess: (saved) => {
|
|
278
279
|
queryClient.setQueryData(["flow", saved.flow.id], saved);
|
|
279
|
-
await queryClient.invalidateQueries({ queryKey: ["flows"] });
|
|
280
280
|
},
|
|
281
281
|
});
|
|
282
282
|
const publish = useMutation({
|
|
@@ -287,10 +287,7 @@ export function Flows() {
|
|
|
287
287
|
idempotencyKey: crypto.randomUUID(),
|
|
288
288
|
}),
|
|
289
289
|
onSuccess: async () => {
|
|
290
|
-
await
|
|
291
|
-
queryClient.invalidateQueries({ queryKey: ["flows"] }),
|
|
292
|
-
queryClient.invalidateQueries({ queryKey: ["flow", selectedFlowId] }),
|
|
293
|
-
]);
|
|
290
|
+
await queryClient.invalidateQueries({ queryKey: ["flow", selectedFlowId] });
|
|
294
291
|
},
|
|
295
292
|
});
|
|
296
293
|
const run = useMutation({
|
|
@@ -311,80 +308,56 @@ export function Flows() {
|
|
|
311
308
|
}
|
|
312
309
|
|
|
313
310
|
return (
|
|
314
|
-
<div className="flex h-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
</div>
|
|
320
|
-
<div className="flex items-center gap-2">
|
|
321
|
-
{selectedFlowId && (
|
|
322
|
-
<>
|
|
323
|
-
<button
|
|
324
|
-
type="button"
|
|
325
|
-
onClick={() => setSharing(true)}
|
|
326
|
-
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"
|
|
327
|
-
>
|
|
328
|
-
<Share2 aria-hidden="true" className="size-4" /> {i18n.t("flows.share")}
|
|
329
|
-
</button>
|
|
330
|
-
<button
|
|
331
|
-
type="button"
|
|
332
|
-
onClick={() => save.mutate()}
|
|
333
|
-
disabled={!canMutate || save.isPending}
|
|
334
|
-
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 disabled:opacity-50"
|
|
335
|
-
>
|
|
336
|
-
<Save aria-hidden="true" className="size-4" /> {i18n.t("common.save")}
|
|
337
|
-
</button>
|
|
338
|
-
<button
|
|
339
|
-
type="button"
|
|
340
|
-
onClick={() => publish.mutate()}
|
|
341
|
-
disabled={!canMutate || !document.data?.flow.currentVersionId || publish.isPending}
|
|
342
|
-
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 disabled:opacity-50"
|
|
343
|
-
>
|
|
344
|
-
<Send aria-hidden="true" className="size-4" /> {i18n.t("flows.publish")}
|
|
345
|
-
</button>
|
|
346
|
-
<button
|
|
347
|
-
type="button"
|
|
348
|
-
onClick={() => run.mutate()}
|
|
349
|
-
disabled={!canMutate || !document.data?.flow.publishedVersionId || run.isPending}
|
|
350
|
-
className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
351
|
-
>
|
|
352
|
-
<Bot aria-hidden="true" className="size-4" /> {i18n.t("flows.run")}
|
|
353
|
-
</button>
|
|
354
|
-
</>
|
|
355
|
-
)}
|
|
311
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
312
|
+
{/* A flow's own actions, in the shell's action bar. Creating a flow is not among them: that
|
|
313
|
+
belongs to the tree, at the folder the flow is meant to live in. */}
|
|
314
|
+
{selectedFlowId ? (
|
|
315
|
+
<HeaderActions>
|
|
356
316
|
<button
|
|
357
317
|
type="button"
|
|
358
|
-
onClick={() =>
|
|
359
|
-
className="inline-flex items-center gap-2 rounded-md bg-
|
|
318
|
+
onClick={() => setSharing(true)}
|
|
319
|
+
className="inline-flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
360
320
|
>
|
|
361
|
-
<
|
|
321
|
+
<Share2 aria-hidden="true" className="size-4" /> {i18n.t("flows.share")}
|
|
362
322
|
</button>
|
|
363
|
-
|
|
364
|
-
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
onClick={() => save.mutate()}
|
|
326
|
+
disabled={!canMutate || save.isPending}
|
|
327
|
+
className="inline-flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
328
|
+
>
|
|
329
|
+
<Save aria-hidden="true" className="size-4" /> {i18n.t("common.save")}
|
|
330
|
+
</button>
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onClick={() => publish.mutate()}
|
|
334
|
+
disabled={!canMutate || !document.data?.flow.currentVersionId || publish.isPending}
|
|
335
|
+
className="inline-flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
336
|
+
>
|
|
337
|
+
<Send aria-hidden="true" className="size-4" /> {i18n.t("flows.publish")}
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
onClick={() => run.mutate()}
|
|
342
|
+
disabled={!canMutate || !document.data?.flow.publishedVersionId || run.isPending}
|
|
343
|
+
className="inline-flex h-8 items-center gap-2 rounded-md bg-primary px-2.5 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
344
|
+
>
|
|
345
|
+
<Bot aria-hidden="true" className="size-4" /> {i18n.t("flows.run")}
|
|
346
|
+
</button>
|
|
347
|
+
</HeaderActions>
|
|
348
|
+
) : null}
|
|
365
349
|
<div className="flex min-h-0 flex-1">
|
|
366
|
-
<aside className="w-64 shrink-0 overflow-y-auto border-r bg-muted/20 p-3">
|
|
367
|
-
{flows.data?.items.map((flow) => (
|
|
368
|
-
<button
|
|
369
|
-
key={flow.id}
|
|
370
|
-
type="button"
|
|
371
|
-
onClick={() => setSelectedFlowId(flow.id)}
|
|
372
|
-
className={`mb-1 w-full rounded-md p-3 text-left outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring ${selectedFlowId === flow.id ? "bg-accent text-accent-foreground" : ""}`}
|
|
373
|
-
>
|
|
374
|
-
<span className="block truncate text-sm font-medium">{flow.title}</span>
|
|
375
|
-
<span className="mt-1 block text-xs text-muted-foreground">
|
|
376
|
-
{flow.publishedVersionId ? i18n.t("flows.published") : i18n.t("flows.draft")}
|
|
377
|
-
</span>
|
|
378
|
-
</button>
|
|
379
|
-
))}
|
|
380
|
-
{flows.data?.items.length === 0 && (
|
|
381
|
-
<p className="p-3 text-sm text-muted-foreground">{i18n.t("flows.empty")}</p>
|
|
382
|
-
)}
|
|
383
|
-
</aside>
|
|
384
350
|
{!selectedFlowId ? (
|
|
385
351
|
<div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
|
|
386
352
|
{i18n.t("flows.select")}
|
|
387
353
|
</div>
|
|
354
|
+
) : document.isError ? (
|
|
355
|
+
<div
|
|
356
|
+
role="alert"
|
|
357
|
+
className="grid flex-1 place-items-center p-8 text-center text-sm text-destructive"
|
|
358
|
+
>
|
|
359
|
+
{i18n.t("flows.operationFailed")}
|
|
360
|
+
</div>
|
|
388
361
|
) : !documentReady ? (
|
|
389
362
|
<div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
|
|
390
363
|
{i18n.t("common.loading")}
|
|
@@ -402,7 +375,7 @@ export function Flows() {
|
|
|
402
375
|
key={kind}
|
|
403
376
|
type="button"
|
|
404
377
|
onClick={() => {
|
|
405
|
-
const item = newNode(kind, nodes.length, knowledge.data?.[0]?.id);
|
|
378
|
+
const item = newNode(kind, nodes.length, knowledge.data?.nodes[0]?.id);
|
|
406
379
|
setNodes((current) => [
|
|
407
380
|
...current,
|
|
408
381
|
{
|
|
@@ -449,7 +422,7 @@ export function Flows() {
|
|
|
449
422
|
node={selectedNode}
|
|
450
423
|
update={updateNode}
|
|
451
424
|
tools={tools.data?.items ?? []}
|
|
452
|
-
knowledge={knowledge.data ?? []}
|
|
425
|
+
knowledge={knowledge.data?.nodes ?? []}
|
|
453
426
|
/>
|
|
454
427
|
</>
|
|
455
428
|
)}
|
|
@@ -470,7 +443,6 @@ export function Flows() {
|
|
|
470
443
|
{i18n.t("flows.operationFailed")}
|
|
471
444
|
</p>
|
|
472
445
|
)}
|
|
473
|
-
{creating && <CreateFlow close={() => setCreating(false)} select={setSelectedFlowId} />}
|
|
474
446
|
{sharing && selectedFlowId && (
|
|
475
447
|
<ShareFlow flowId={selectedFlowId} close={() => setSharing(false)} />
|
|
476
448
|
)}
|
|
@@ -599,7 +571,7 @@ function NodeInspector({
|
|
|
599
571
|
node: CanvasNode | null;
|
|
600
572
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
601
573
|
tools: Array<{ name: string; title: string | null; fingerprint: string }>;
|
|
602
|
-
knowledge:
|
|
574
|
+
knowledge: KnowledgeNode[];
|
|
603
575
|
}) {
|
|
604
576
|
const { i18n } = useIntelRouterContext();
|
|
605
577
|
if (!node)
|
|
@@ -659,11 +631,10 @@ function NodeInspector({
|
|
|
659
631
|
<>
|
|
660
632
|
<fieldset className="mt-4 rounded-lg border p-3">
|
|
661
633
|
<legend className="px-1 text-sm font-medium">{i18n.t("flows.knowledgeIds")}</legend>
|
|
662
|
-
{
|
|
634
|
+
{knowledge.map((item) => (
|
|
663
635
|
<label
|
|
664
636
|
key={item.id}
|
|
665
637
|
className="flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted"
|
|
666
|
-
style={{ paddingLeft: `${item.depth * 12 + 8}px` }}
|
|
667
638
|
>
|
|
668
639
|
<input
|
|
669
640
|
type="checkbox"
|
|
@@ -812,16 +783,6 @@ function ToolArgumentsEditor({
|
|
|
812
783
|
);
|
|
813
784
|
}
|
|
814
785
|
|
|
815
|
-
function flattenKnowledge(
|
|
816
|
-
nodes: KnowledgeTreeNode[],
|
|
817
|
-
depth = 0,
|
|
818
|
-
): Array<{ id: string; title: string; depth: number }> {
|
|
819
|
-
return nodes.flatMap((node) => [
|
|
820
|
-
{ id: node.id, title: node.title, depth },
|
|
821
|
-
...flattenKnowledge(node.children, depth + 1),
|
|
822
|
-
]);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
786
|
function Field({
|
|
826
787
|
label,
|
|
827
788
|
value,
|
|
@@ -863,47 +824,3 @@ function TextArea({
|
|
|
863
824
|
</label>
|
|
864
825
|
);
|
|
865
826
|
}
|
|
866
|
-
|
|
867
|
-
function CreateFlow({ close, select }: { close(): void; select(id: string): void }) {
|
|
868
|
-
const { data, i18n } = useIntelRouterContext();
|
|
869
|
-
const queryClient = useQueryClient();
|
|
870
|
-
const [title, setTitle] = useState("");
|
|
871
|
-
const create = useMutation({
|
|
872
|
-
mutationFn: () =>
|
|
873
|
-
data.createFlow({
|
|
874
|
-
title,
|
|
875
|
-
description: null,
|
|
876
|
-
idempotencyKey: crypto.randomUUID(),
|
|
877
|
-
}),
|
|
878
|
-
onSuccess: async (flow) => {
|
|
879
|
-
await queryClient.invalidateQueries({ queryKey: ["flows"] });
|
|
880
|
-
select(flow.id);
|
|
881
|
-
close();
|
|
882
|
-
},
|
|
883
|
-
});
|
|
884
|
-
return (
|
|
885
|
-
<Modal title={i18n.t("flows.new")} close={close}>
|
|
886
|
-
<form
|
|
887
|
-
onSubmit={(event) => {
|
|
888
|
-
event.preventDefault();
|
|
889
|
-
if (create.isPending) return;
|
|
890
|
-
create.mutate();
|
|
891
|
-
}}
|
|
892
|
-
>
|
|
893
|
-
<Field label={i18n.t("common.title")} value={title} setValue={setTitle} />
|
|
894
|
-
{create.isError ? (
|
|
895
|
-
<p role="alert" className="mt-3 text-sm text-destructive">
|
|
896
|
-
{i18n.t("flows.operationFailed")}
|
|
897
|
-
</p>
|
|
898
|
-
) : null}
|
|
899
|
-
<button
|
|
900
|
-
type="submit"
|
|
901
|
-
disabled={create.isPending || title.trim().length === 0}
|
|
902
|
-
className="mt-5 w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60"
|
|
903
|
-
>
|
|
904
|
-
{create.isPending ? i18n.t("common.saving") : i18n.t("common.create")}
|
|
905
|
-
</button>
|
|
906
|
-
</form>
|
|
907
|
-
</Modal>
|
|
908
|
-
);
|
|
909
|
-
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
12
|
+
};
|
|
13
|
+
mql.addEventListener("change", onChange);
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
15
|
+
return () => mql.removeEventListener("change", onChange);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return !!isMobile;
|
|
19
|
+
}
|