@anchrd/intel-ui 0.5.0 → 0.7.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 +2 -2
- package/src/app/action-slot/action-slot.tsx +4 -0
- package/src/app/app-tree/app-tree.tsx +35 -122
- package/src/app/app.tsx +15 -4
- package/src/app/tree-move/tree-move.tsx +135 -1
- package/src/components/ui/table.tsx +82 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +19 -3
- package/src/data/intel-data-provider/intel-data-provider.types.ts +7 -1
- package/src/entry-picker/entry-picker.tsx +166 -0
- package/src/flows/flows.tsx +183 -183
- package/src/flows/node-icon/node-icon.ts +13 -7
- package/src/flows/node-palette/node-palette.tsx +63 -23
- package/src/folder-contents/folder-contents.tsx +106 -0
- package/src/i18n/en.json +36 -19
- package/src/knowledge/knowledge.tsx +47 -38
- package/src/knowledge-table/knowledge-table.tsx +23 -11
- package/src/resource-menu/resource-menu.tsx +110 -65
- package/src/title-row/title-row.tsx +49 -0
- package/src/tools/tools.tsx +57 -38
package/src/flows/flows.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Flow, FlowGraph, FlowNode, KnowledgeNode } from "@anchrd/intel-contract";
|
|
2
|
+
import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
2
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
4
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
4
5
|
import {
|
|
@@ -17,22 +18,26 @@ import {
|
|
|
17
18
|
type NodeProps,
|
|
18
19
|
Position,
|
|
19
20
|
ReactFlow,
|
|
21
|
+
ReactFlowProvider,
|
|
22
|
+
useReactFlow,
|
|
20
23
|
} from "@xyflow/react";
|
|
21
24
|
import { FileSearch, Info, Send, Wrench } from "lucide-react";
|
|
22
25
|
import { useEffect, useId, useMemo, useState } from "react";
|
|
23
26
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
27
|
+
import { TreeEntryMediaType } from "@/app/app-tree/app-tree.tsx";
|
|
24
28
|
import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
|
|
25
29
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
30
|
+
import { EntryPicker } from "@/entry-picker/entry-picker.tsx";
|
|
26
31
|
import { FlowRuns } from "@/flow-runs/flow-runs.tsx";
|
|
27
32
|
import { nodeIcon } from "@/flows/node-icon/node-icon.ts";
|
|
28
33
|
import { NodePalette, usePaletteOpen } from "@/flows/node-palette/node-palette.tsx";
|
|
29
34
|
import { GraphPane } from "@/graph-pane/graph-pane.tsx";
|
|
30
35
|
import { Modal } from "@/modal/modal.tsx";
|
|
31
|
-
import { ResourceMenu } from "@/resource-menu/resource-menu.tsx";
|
|
32
36
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
33
37
|
import { selectedFrom, viewFrom } from "@/router/selection-search.ts";
|
|
34
38
|
import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
35
39
|
import { useSystemTheme } from "@/theme/theme.ts";
|
|
40
|
+
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
36
41
|
|
|
37
42
|
type CanvasNode = Node<{ node: FlowNode }, "intel">;
|
|
38
43
|
type CanvasEdge = Edge;
|
|
@@ -48,19 +53,46 @@ const ContextHandle = "context";
|
|
|
48
53
|
const paletteKinds = [
|
|
49
54
|
"trigger",
|
|
50
55
|
"instruction",
|
|
51
|
-
"knowledge",
|
|
52
|
-
"tool",
|
|
53
56
|
"condition",
|
|
54
|
-
"approval",
|
|
55
57
|
"subflow",
|
|
58
|
+
"folder",
|
|
59
|
+
"document",
|
|
60
|
+
"upload",
|
|
61
|
+
"table",
|
|
62
|
+
"tool",
|
|
56
63
|
"output",
|
|
57
64
|
] as const;
|
|
58
65
|
|
|
66
|
+
// What the tree hands over, read back defensively: the payload crosses a browser API, so it is
|
|
67
|
+
// parsed rather than trusted. A drop that does not carry what we put there is simply ignored.
|
|
68
|
+
function droppedEntry(payload: string): { id: string; kind: string; title: string } | null {
|
|
69
|
+
try {
|
|
70
|
+
const parsed: unknown = JSON.parse(payload);
|
|
71
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
72
|
+
const { id, kind, title } = parsed as Record<string, unknown>;
|
|
73
|
+
if (typeof id !== "string" || typeof kind !== "string" || typeof title !== "string")
|
|
74
|
+
return null;
|
|
75
|
+
return { id, kind, title };
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A row's kind decides what dropping it makes, so nothing has to be asked. A folder becomes a
|
|
82
|
+
// Folder link, a flow becomes the Flow step that calls it (D25).
|
|
83
|
+
const flowKindOfEntry: Record<string, FlowNode["kind"] | undefined> = {
|
|
84
|
+
folder: "folder",
|
|
85
|
+
document: "document",
|
|
86
|
+
attachment: "upload",
|
|
87
|
+
table: "table",
|
|
88
|
+
flow: "subflow",
|
|
89
|
+
};
|
|
90
|
+
|
|
59
91
|
function FlowCard({ data, selected }: NodeProps<CanvasNode>) {
|
|
60
92
|
const { i18n } = useIntelRouterContext();
|
|
61
93
|
const contract = data.node;
|
|
62
94
|
const Icon = nodeIcon[contract.kind];
|
|
63
|
-
const branching = contract.kind === "condition"
|
|
95
|
+
const branching = contract.kind === "condition";
|
|
64
96
|
// ⚠️ The start is told apart by silhouette first (#39): a pill among rectangles, in the primary
|
|
65
97
|
// token. Shape survives zooming out past the point where the label is legible, and it is the only
|
|
66
98
|
// one of the three that also carries into the overview map, where nothing is written at all.
|
|
@@ -99,18 +131,8 @@ function FlowCard({ data, selected }: NodeProps<CanvasNode>) {
|
|
|
99
131
|
)}
|
|
100
132
|
{branching && (
|
|
101
133
|
<>
|
|
102
|
-
<Handle
|
|
103
|
-
|
|
104
|
-
type="source"
|
|
105
|
-
position={Position.Right}
|
|
106
|
-
style={{ top: "35%" }}
|
|
107
|
-
/>
|
|
108
|
-
<Handle
|
|
109
|
-
id={contract.kind === "approval" ? "rejected" : "no"}
|
|
110
|
-
type="source"
|
|
111
|
-
position={Position.Right}
|
|
112
|
-
style={{ top: "70%" }}
|
|
113
|
-
/>
|
|
134
|
+
<Handle id="yes" type="source" position={Position.Right} style={{ top: "35%" }} />
|
|
135
|
+
<Handle id="no" type="source" position={Position.Right} style={{ top: "70%" }} />
|
|
114
136
|
</>
|
|
115
137
|
)}
|
|
116
138
|
{/* The context point (#37). Sideways is the order of work, downwards is what a step works
|
|
@@ -143,7 +165,7 @@ function defaultGraph(): FlowGraph {
|
|
|
143
165
|
kind: "output",
|
|
144
166
|
label: "Result",
|
|
145
167
|
position: { x: 520, y: 180 },
|
|
146
|
-
configuration: {
|
|
168
|
+
configuration: {},
|
|
147
169
|
},
|
|
148
170
|
],
|
|
149
171
|
edges: [
|
|
@@ -236,6 +258,27 @@ function editsEdges(changes: EdgeChange<CanvasEdge>[]): boolean {
|
|
|
236
258
|
);
|
|
237
259
|
}
|
|
238
260
|
|
|
261
|
+
// ⚠️ `upload` is what the flow calls an attachment. The two names are deliberately not unified:
|
|
262
|
+
// in the tree it is a file somebody uploaded, in a flow it is material a step reads, and renaming
|
|
263
|
+
// either one to match the other would make the other read wrong.
|
|
264
|
+
//
|
|
265
|
+
// The reverse map is DERIVED rather than written a second time — two hand-kept tables of the same
|
|
266
|
+
// correspondence are two tables that eventually disagree, and the disagreement would show up as a
|
|
267
|
+
// picker offering the wrong kind.
|
|
268
|
+
const nodeKindOfLink = Object.fromEntries(
|
|
269
|
+
Object.entries(flowKindOfEntry)
|
|
270
|
+
.filter(([, kind]) => kind !== undefined && kind !== "subflow")
|
|
271
|
+
.map(([nodeKind, kind]) => [kind, nodeKind]),
|
|
272
|
+
) as Partial<Record<FlowNode["kind"], KnowledgeNode["kind"]>>;
|
|
273
|
+
|
|
274
|
+
type LinkNode = Extract<FlowNode, { configuration: { resourceId: string } }>;
|
|
275
|
+
|
|
276
|
+
// A predicate rather than a boolean, so the update below can build the one configuration a link
|
|
277
|
+
// has without widening every other node's to match.
|
|
278
|
+
function isLinkNode(node: FlowNode): node is LinkNode {
|
|
279
|
+
return flowNodeLayer[node.kind] === "link" && "resourceId" in node.configuration;
|
|
280
|
+
}
|
|
281
|
+
|
|
239
282
|
function newNode(
|
|
240
283
|
kind: FlowNode["kind"],
|
|
241
284
|
index: number,
|
|
@@ -259,16 +302,14 @@ function newNode(
|
|
|
259
302
|
kind,
|
|
260
303
|
configuration: { prompt: "Describe what the AI should do." },
|
|
261
304
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
},
|
|
271
|
-
};
|
|
305
|
+
// The four link kinds differ only in what the picker offers, so they are one branch. A link
|
|
306
|
+
// starts out naming nothing: it is filled in from the inspector, and `compileFlow` refuses to
|
|
307
|
+
// publish a graph whose reference is empty.
|
|
308
|
+
case "folder":
|
|
309
|
+
case "document":
|
|
310
|
+
case "upload":
|
|
311
|
+
case "table":
|
|
312
|
+
return { ...common, kind, configuration: { resourceId: firstKnowledgeId ?? "" } };
|
|
272
313
|
case "tool":
|
|
273
314
|
return {
|
|
274
315
|
...common,
|
|
@@ -288,15 +329,6 @@ function newNode(
|
|
|
288
329
|
instruction: "Describe how to choose yes or no.",
|
|
289
330
|
},
|
|
290
331
|
};
|
|
291
|
-
case "approval":
|
|
292
|
-
return {
|
|
293
|
-
...common,
|
|
294
|
-
kind,
|
|
295
|
-
configuration: {
|
|
296
|
-
prompt: "Describe what must be approved.",
|
|
297
|
-
timeout: "7 days",
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
332
|
case "subflow":
|
|
301
333
|
// `latest` while drafting: nobody should have to version things while building, and
|
|
302
334
|
// publishing turns it into the concrete version (ADR-0004 §5).
|
|
@@ -306,12 +338,24 @@ function newNode(
|
|
|
306
338
|
configuration: { flowId: firstFlowId ?? "", version: { mode: "latest" } },
|
|
307
339
|
};
|
|
308
340
|
case "output":
|
|
309
|
-
return { ...common, kind, configuration: {
|
|
341
|
+
return { ...common, kind, configuration: {} };
|
|
310
342
|
}
|
|
311
343
|
}
|
|
312
344
|
|
|
345
|
+
// ⚠️ The provider wraps the editor rather than sitting inside it: `useReactFlow` — which is how the
|
|
346
|
+
// drop turns a screen point into a graph point — has to be called from a component UNDER it, and
|
|
347
|
+
// the hook lives in the same component that renders the canvas.
|
|
313
348
|
export function Flows() {
|
|
349
|
+
return (
|
|
350
|
+
<ReactFlowProvider>
|
|
351
|
+
<FlowsEditor />
|
|
352
|
+
</ReactFlowProvider>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function FlowsEditor() {
|
|
314
357
|
const { data, i18n } = useIntelRouterContext();
|
|
358
|
+
const flow = useReactFlow();
|
|
315
359
|
const theme = useSystemTheme();
|
|
316
360
|
const queryClient = useQueryClient();
|
|
317
361
|
const navigate = useNavigate();
|
|
@@ -398,15 +442,6 @@ export function Flows() {
|
|
|
398
442
|
]);
|
|
399
443
|
},
|
|
400
444
|
});
|
|
401
|
-
const run = useMutation({
|
|
402
|
-
mutationFn: () =>
|
|
403
|
-
data.startFlow({
|
|
404
|
-
flowId: selectedFlowId ?? "",
|
|
405
|
-
input: {},
|
|
406
|
-
parent: null,
|
|
407
|
-
idempotencyKey: crypto.randomUUID(),
|
|
408
|
-
}),
|
|
409
|
-
});
|
|
410
445
|
|
|
411
446
|
function updateNode(update: (node: FlowNode) => FlowNode) {
|
|
412
447
|
setDirty(true);
|
|
@@ -435,10 +470,8 @@ export function Flows() {
|
|
|
435
470
|
dirty={dirty}
|
|
436
471
|
saving={save.isPending}
|
|
437
472
|
canMutate={canMutate}
|
|
438
|
-
running={run.isPending}
|
|
439
473
|
onSave={() => save.mutate()}
|
|
440
474
|
onPublish={() => setPublishing(true)}
|
|
441
|
-
onRun={() => run.mutate()}
|
|
442
475
|
/>
|
|
443
476
|
) : null}
|
|
444
477
|
<UnsavedChangesGuard dirty={dirty} />
|
|
@@ -551,6 +584,50 @@ export function Flows() {
|
|
|
551
584
|
setSelectedNodeId(null);
|
|
552
585
|
setPaletteOpen(false);
|
|
553
586
|
}}
|
|
587
|
+
// ⚠️ `copy`, not `move` (#75). The tree's own drop targets say `move`, and the two
|
|
588
|
+
// have to feel different while the pointer is still travelling: dropping here makes
|
|
589
|
+
// a node that POINTS at the row, it does not take the row out of its folder.
|
|
590
|
+
onDragOver={(event) => {
|
|
591
|
+
if (!event.dataTransfer.types.includes(TreeEntryMediaType)) return;
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
event.dataTransfer.dropEffect = "copy";
|
|
594
|
+
}}
|
|
595
|
+
onDrop={(event) => {
|
|
596
|
+
const payload = event.dataTransfer.getData(TreeEntryMediaType);
|
|
597
|
+
if (!payload) return;
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
const dropped = droppedEntry(payload);
|
|
600
|
+
if (!dropped) return;
|
|
601
|
+
const kind = flowKindOfEntry[dropped.kind];
|
|
602
|
+
if (!kind) return;
|
|
603
|
+
// Where the pointer let go, in the graph's own coordinates — not the screen's, or
|
|
604
|
+
// the node would land somewhere else at every zoom level.
|
|
605
|
+
const position = flow.screenToFlowPosition({
|
|
606
|
+
x: event.clientX,
|
|
607
|
+
y: event.clientY,
|
|
608
|
+
});
|
|
609
|
+
setDirty(true);
|
|
610
|
+
setNodes((current) => [
|
|
611
|
+
...current,
|
|
612
|
+
{
|
|
613
|
+
id: `${kind}-${crypto.randomUUID()}`,
|
|
614
|
+
type: "intel" as const,
|
|
615
|
+
position,
|
|
616
|
+
data: {
|
|
617
|
+
node: {
|
|
618
|
+
id: `${kind}-${crypto.randomUUID()}`,
|
|
619
|
+
kind,
|
|
620
|
+
label: dropped.title,
|
|
621
|
+
position,
|
|
622
|
+
configuration:
|
|
623
|
+
kind === "subflow"
|
|
624
|
+
? { flowId: dropped.id, version: { mode: "latest" as const } }
|
|
625
|
+
: { resourceId: dropped.id },
|
|
626
|
+
} as FlowNode,
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
]);
|
|
630
|
+
}}
|
|
554
631
|
>
|
|
555
632
|
<Background />
|
|
556
633
|
{/* The overview map has no room for a label, so the start is marked there the only
|
|
@@ -570,7 +647,6 @@ export function Flows() {
|
|
|
570
647
|
node={selectedNode}
|
|
571
648
|
update={updateNode}
|
|
572
649
|
tools={tools.data?.items ?? []}
|
|
573
|
-
knowledge={knowledge.data?.nodes ?? []}
|
|
574
650
|
flows={(callable.data?.items ?? []).filter((entry) => entry.id !== selectedFlowId)}
|
|
575
651
|
/>
|
|
576
652
|
<FlowNeeds flowId={selectedFlowId} />
|
|
@@ -578,15 +654,7 @@ export function Flows() {
|
|
|
578
654
|
</>
|
|
579
655
|
)}
|
|
580
656
|
</div>
|
|
581
|
-
{
|
|
582
|
-
<div className="fixed bottom-5 right-5 z-20 max-w-sm rounded-xl border bg-card p-4 text-sm shadow-xl">
|
|
583
|
-
<p className="font-medium">{i18n.t("flows.runStarted")}</p>
|
|
584
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
585
|
-
{run.data.node?.label ?? run.data.run.status} · {run.data.run.id}
|
|
586
|
-
</p>
|
|
587
|
-
</div>
|
|
588
|
-
)}
|
|
589
|
-
{(save.isError || run.isError) && (
|
|
657
|
+
{save.isError && (
|
|
590
658
|
<p
|
|
591
659
|
role="alert"
|
|
592
660
|
className="fixed bottom-5 left-[18rem] z-20 rounded-lg border border-destructive/30 bg-card px-4 py-3 text-sm text-destructive shadow-xl"
|
|
@@ -636,19 +704,15 @@ function FlowTitle({
|
|
|
636
704
|
dirty,
|
|
637
705
|
saving,
|
|
638
706
|
canMutate,
|
|
639
|
-
running,
|
|
640
707
|
onSave,
|
|
641
708
|
onPublish,
|
|
642
|
-
onRun,
|
|
643
709
|
}: {
|
|
644
710
|
flow: Flow;
|
|
645
711
|
dirty: boolean;
|
|
646
712
|
saving: boolean;
|
|
647
713
|
canMutate: boolean;
|
|
648
|
-
running: boolean;
|
|
649
714
|
onSave(): void;
|
|
650
715
|
onPublish(): void;
|
|
651
|
-
onRun(): void;
|
|
652
716
|
}) {
|
|
653
717
|
const { i18n } = useIntelRouterContext();
|
|
654
718
|
// A draft that is already the published one freezes nothing, so publishing it would be a no-op
|
|
@@ -664,49 +728,36 @@ function FlowTitle({
|
|
|
664
728
|
!publishable ? "flows.publishNothing" : unpublished ? "flows.publishNeeded" : "flows.publish",
|
|
665
729
|
);
|
|
666
730
|
return (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
<TooltipTrigger asChild>
|
|
678
|
-
<span className="inline-flex">
|
|
679
|
-
{/* Publishing goes through the preview, always. ADR-0004 §5 asks the author to see
|
|
731
|
+
// ⚠️ The menu stands to the RIGHT of the run button, not before it (#53, deliberate). Run is the
|
|
732
|
+
// loudest thing in this line and used to hold the edge; the menu takes it, because a menu one can
|
|
733
|
+
// hit without looking is worth more than the primary button being outermost. `TitleRow` is what
|
|
734
|
+
// enforces that — nothing passed in here can get past the menu.
|
|
735
|
+
<TitleRow title={flow.title} description={flow.description} target={{ type: "flow", flow }}>
|
|
736
|
+
<TooltipProvider delayDuration={300}>
|
|
737
|
+
<Tooltip>
|
|
738
|
+
<TooltipTrigger asChild>
|
|
739
|
+
<span className="inline-flex">
|
|
740
|
+
{/* Publishing goes through the preview, always. ADR-0004 §5 asks the author to see
|
|
680
741
|
the freeze before it happens, and a second, quieter path around the dialog would
|
|
681
742
|
be the one everybody ends up using. */}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
<button
|
|
701
|
-
type="button"
|
|
702
|
-
onClick={onRun}
|
|
703
|
-
disabled={!canMutate || unpublished || running}
|
|
704
|
-
className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
705
|
-
>
|
|
706
|
-
{i18n.t("flows.run")}
|
|
707
|
-
</button>
|
|
708
|
-
</div>
|
|
709
|
-
</div>
|
|
743
|
+
<button
|
|
744
|
+
type="button"
|
|
745
|
+
onClick={onPublish}
|
|
746
|
+
disabled={!publishable}
|
|
747
|
+
aria-label={publishLabel}
|
|
748
|
+
className={`inline-flex size-8 items-center justify-center rounded-md border bg-background outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 ${
|
|
749
|
+
publishable && unpublished ? "border-primary text-primary" : ""
|
|
750
|
+
}`}
|
|
751
|
+
>
|
|
752
|
+
<Send aria-hidden="true" className="size-4" />
|
|
753
|
+
</button>
|
|
754
|
+
</span>
|
|
755
|
+
</TooltipTrigger>
|
|
756
|
+
<TooltipContent>{publishLabel}</TooltipContent>
|
|
757
|
+
</Tooltip>
|
|
758
|
+
</TooltipProvider>
|
|
759
|
+
<SaveButton dirty={dirty && canMutate} saving={saving} onSave={onSave} />
|
|
760
|
+
</TitleRow>
|
|
710
761
|
);
|
|
711
762
|
}
|
|
712
763
|
|
|
@@ -800,13 +851,11 @@ function NodeInspector({
|
|
|
800
851
|
node,
|
|
801
852
|
update,
|
|
802
853
|
tools,
|
|
803
|
-
knowledge,
|
|
804
854
|
flows,
|
|
805
855
|
}: {
|
|
806
856
|
node: CanvasNode | null;
|
|
807
857
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
808
858
|
tools: Array<{ name: string; title: string | null; fingerprint: string }>;
|
|
809
|
-
knowledge: KnowledgeNode[];
|
|
810
859
|
flows: Flow[];
|
|
811
860
|
}) {
|
|
812
861
|
const { i18n } = useIntelRouterContext();
|
|
@@ -815,23 +864,24 @@ function NodeInspector({
|
|
|
815
864
|
return (
|
|
816
865
|
<div className="p-5">
|
|
817
866
|
<h2 className="font-semibold">{i18n.t(`flows.node.${contract.kind}`)}</h2>
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
867
|
+
{/* ⚠️ A marker has no name to give (D25). Start and End are the same two points in every
|
|
868
|
+
flow, and a start renamed "Rechnung holen" reads on the canvas as a step that does
|
|
869
|
+
something — which is exactly the confusion the layer split is meant to end. */}
|
|
870
|
+
{flowNodeLayer[contract.kind] !== "marker" && (
|
|
871
|
+
<Field
|
|
872
|
+
label={i18n.t("common.title")}
|
|
873
|
+
value={contract.label}
|
|
874
|
+
setValue={(label) => update((value) => ({ ...value, label }))}
|
|
875
|
+
/>
|
|
876
|
+
)}
|
|
877
|
+
{(contract.kind === "instruction" || contract.kind === "condition") && (
|
|
826
878
|
<TextArea
|
|
827
879
|
label={i18n.t("flows.instruction")}
|
|
828
880
|
hint={i18n.t("flows.instructionHint")}
|
|
829
881
|
value={
|
|
830
882
|
contract.kind === "instruction"
|
|
831
883
|
? contract.configuration.prompt
|
|
832
|
-
: contract.
|
|
833
|
-
? contract.configuration.instruction
|
|
834
|
-
: contract.configuration.prompt
|
|
884
|
+
: contract.configuration.instruction
|
|
835
885
|
}
|
|
836
886
|
setValue={(text) =>
|
|
837
887
|
update((value) =>
|
|
@@ -842,76 +892,26 @@ function NodeInspector({
|
|
|
842
892
|
...value,
|
|
843
893
|
configuration: { mode: "semantic", instruction: text },
|
|
844
894
|
}
|
|
845
|
-
: value
|
|
846
|
-
? {
|
|
847
|
-
...value,
|
|
848
|
-
configuration: { ...value.configuration, prompt: text },
|
|
849
|
-
}
|
|
850
|
-
: value,
|
|
895
|
+
: value,
|
|
851
896
|
)
|
|
852
897
|
}
|
|
853
898
|
/>
|
|
854
899
|
)}
|
|
855
|
-
{
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
onChange={(event) =>
|
|
872
|
-
update((value) => {
|
|
873
|
-
if (value.kind !== "knowledge") return value;
|
|
874
|
-
const selected = new Set(value.configuration.resourceIds);
|
|
875
|
-
if (event.target.checked) selected.add(item.id);
|
|
876
|
-
else selected.delete(item.id);
|
|
877
|
-
return {
|
|
878
|
-
...value,
|
|
879
|
-
configuration: {
|
|
880
|
-
...value.configuration,
|
|
881
|
-
resourceIds: [...selected],
|
|
882
|
-
},
|
|
883
|
-
};
|
|
884
|
-
})
|
|
885
|
-
}
|
|
886
|
-
className="mt-0.5 size-4 accent-primary disabled:cursor-not-allowed disabled:opacity-50"
|
|
887
|
-
/>
|
|
888
|
-
<span className="min-w-0 truncate">{item.title}</span>
|
|
889
|
-
</label>
|
|
890
|
-
))}
|
|
891
|
-
{knowledge.length === 0 && (
|
|
892
|
-
<p className="px-2 py-1 text-sm text-muted-foreground">
|
|
893
|
-
{i18n.t("flows.knowledgeEmpty")}
|
|
894
|
-
</p>
|
|
895
|
-
)}
|
|
896
|
-
</fieldset>
|
|
897
|
-
<TextArea
|
|
898
|
-
label={i18n.t("flows.knowledgeQuery")}
|
|
899
|
-
value={contract.configuration.query ?? ""}
|
|
900
|
-
setValue={(query) =>
|
|
901
|
-
update((value) =>
|
|
902
|
-
value.kind === "knowledge"
|
|
903
|
-
? {
|
|
904
|
-
...value,
|
|
905
|
-
configuration: {
|
|
906
|
-
...value.configuration,
|
|
907
|
-
query: query || null,
|
|
908
|
-
},
|
|
909
|
-
}
|
|
910
|
-
: value,
|
|
911
|
-
)
|
|
912
|
-
}
|
|
913
|
-
/>
|
|
914
|
-
</>
|
|
900
|
+
{/* ⚠️ One reference, chosen — not a list, ticked. What replaces this properly is the picker
|
|
901
|
+
in #75 (search, the tree, dropping from the sidebar); until then a select does the one
|
|
902
|
+
thing the layer rule needs it to do, which is make "exactly one" the only sayable state.
|
|
903
|
+
The list is filtered to the kind of the node, so a Table node cannot name a folder. */}
|
|
904
|
+
{isLinkNode(contract) && (
|
|
905
|
+
<EntryPicker
|
|
906
|
+
kind={nodeKindOfLink[contract.kind] ?? null}
|
|
907
|
+
value={contract.configuration.resourceId}
|
|
908
|
+
label={i18n.t(`flows.node.${contract.kind}`)}
|
|
909
|
+
onSelect={(resourceId) =>
|
|
910
|
+
update((value) =>
|
|
911
|
+
isLinkNode(value) ? { ...value, configuration: { resourceId } } : value,
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
/>
|
|
915
915
|
)}
|
|
916
916
|
{contract.kind === "subflow" && (
|
|
917
917
|
<div className="mt-4">
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { FlowNode } from "@anchrd/intel-contract";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
CircleDot,
|
|
4
4
|
CirclePlay,
|
|
5
|
-
|
|
5
|
+
FileText,
|
|
6
|
+
Folder,
|
|
6
7
|
GitBranch,
|
|
7
|
-
|
|
8
|
+
Paperclip,
|
|
8
9
|
Sparkles,
|
|
10
|
+
Table,
|
|
9
11
|
Workflow,
|
|
10
12
|
Wrench,
|
|
11
13
|
} from "lucide-react";
|
|
@@ -19,10 +21,14 @@ export type NodeIcon = ComponentType<SVGProps<SVGSVGElement>>;
|
|
|
19
21
|
export const nodeIcon: Record<FlowNode["kind"], NodeIcon> = {
|
|
20
22
|
trigger: CirclePlay,
|
|
21
23
|
instruction: Sparkles,
|
|
22
|
-
knowledge: FileSearch,
|
|
23
|
-
tool: Wrench,
|
|
24
24
|
condition: GitBranch,
|
|
25
|
-
approval: ShieldCheck,
|
|
26
25
|
subflow: Workflow,
|
|
27
|
-
|
|
26
|
+
// The four links wear the symbol their kind wears in the tree, so a document is the same shape
|
|
27
|
+
// wherever it is seen — the sidebar, the picker and the canvas do not each teach their own.
|
|
28
|
+
folder: Folder,
|
|
29
|
+
document: FileText,
|
|
30
|
+
upload: Paperclip,
|
|
31
|
+
table: Table,
|
|
32
|
+
tool: Wrench,
|
|
33
|
+
output: CircleDot,
|
|
28
34
|
};
|