@anchrd/intel-ui 0.18.0 → 0.20.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 -9
- package/src/app/app-tree/app-tree.tsx +68 -147
- package/src/app/header-search/header-search.tsx +2 -1
- package/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/app/user-footer/user-footer.tsx +19 -0
- package/src/archive/archive.tsx +2 -1
- package/src/data/intel-data-provider/intel-data-provider.ts +42 -191
- package/src/data/intel-data-provider/intel-data-provider.types.ts +36 -120
- package/src/document-link/document-link.tsx +1 -1
- package/src/entry-picker/entry-picker.tsx +7 -7
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +3 -2
- package/src/flows/node-icon/node-icon.ts +1 -1
- package/src/flows/node-palette/node-palette.tsx +1 -1
- package/src/flows/node-palette/node-palette.types.ts +1 -1
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/i18n/de.json +14 -217
- package/src/i18n/en.json +14 -217
- package/src/i18n/es.json +14 -217
- package/src/import-bundle/import-bundle.tsx +152 -0
- package/src/kind-icon.ts +1 -15
- package/src/main.tsx +7 -10
- package/src/node-editor/node-editor.tsx +1 -1
- package/src/node-graph/graph-notice.tsx +1 -1
- package/src/node-graph/node-graph.ts +1 -1
- package/src/node-graph/node-graph.tsx +1 -1
- package/src/node-table/node-table.tsx +3 -3
- package/src/nodes/nodes.tsx +1 -35
- package/src/resource-menu/resource-menu.tsx +45 -21
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +1 -3
- package/vite.config.ts +0 -4
- package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
- package/src/agent/agent-calendar/agent-calendar.tsx +0 -160
- package/src/agent/agent-chat/agent-chat.tsx +0 -118
- package/src/agent/agent-costs/agent-costs.ts +0 -95
- package/src/agent/agent-cron/agent-cron.ts +0 -68
- package/src/agent/agent-definition/agent-definition.ts +0 -96
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
- package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
- package/src/agent/agent-log/agent-log.tsx +0 -308
- package/src/agent/agent-models/agent-models.ts +0 -166
- package/src/agent/agent-models/agent-models.types.ts +0 -24
- package/src/agent/agent-profile/agent-profile.tsx +0 -1006
- package/src/agent/agent-state/agent-state.ts +0 -85
- package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
- package/src/agent/agent.tsx +0 -507
- package/src/board/board-calendar/board-calendar.tsx +0 -89
- package/src/board/board-card/board-card.tsx +0 -106
- package/src/board/board-data/board-data.ts +0 -241
- package/src/board/board-data/board-data.types.ts +0 -63
- package/src/board/board-detail/board-detail.tsx +0 -629
- package/src/board/board-gantt/board-gantt.ts +0 -545
- package/src/board/board-gantt/board-gantt.tsx +0 -286
- package/src/board/board-graph/board-graph.ts +0 -174
- package/src/board/board-graph/board-graph.tsx +0 -168
- package/src/board/board-items/board-items.ts +0 -183
- package/src/board/board-kanban/board-kanban.ts +0 -137
- package/src/board/board-kanban/board-kanban.tsx +0 -257
- package/src/board/board-status/board-status.ts +0 -59
- package/src/board/board-statuses/board-statuses.ts +0 -63
- package/src/board/board-statuses/board-statuses.tsx +0 -228
- package/src/board/board-table/board-table.ts +0 -33
- package/src/board/board-table/board-table.tsx +0 -413
- package/src/board/board-views/board-views.tsx +0 -68
- package/src/board/board-views/board-views.types.ts +0 -29
- package/src/board/board.tsx +0 -251
- package/src/components/ui/item-calendar.tsx +0 -181
- package/src/components/ui/item-gantt.tsx +0 -463
- package/src/components/ui/kanban.tsx +0 -282
- package/src/data/agent-runtime/agent-runtime.ts +0 -131
- package/src/hooks/use-capabilities.ts +0 -22
- package/src/hooks/use-model-catalog.ts +0 -26
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
- package/src/timezone/timezone-context.tsx +0 -65
- package/src/timezone/timezone.ts +0 -174
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -33,27 +33,20 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@anchrd/intel-contract": "^0.
|
|
37
|
-
"@assistant-ui/react": "^0.15.4",
|
|
38
|
-
"@assistant-ui/react-ai-sdk": "^1.4.4",
|
|
36
|
+
"@anchrd/intel-contract": "^0.14.0",
|
|
39
37
|
"@blocknote/core": "^0.52.1",
|
|
40
38
|
"@blocknote/react": "^0.52.1",
|
|
41
39
|
"@blocknote/shadcn": "^0.52.1",
|
|
42
|
-
"@dnd-kit/core": "^6.3.1",
|
|
43
|
-
"@dnd-kit/sortable": "^10.0.0",
|
|
44
|
-
"@dnd-kit/utilities": "^3.2.2",
|
|
45
40
|
"@react-sigma/core": "^5.0.6",
|
|
46
41
|
"@tailwindcss/vite": "^4.3.3",
|
|
47
42
|
"@tanstack/react-query": "^5.101.4",
|
|
48
43
|
"@tanstack/react-router": "^1.170.18",
|
|
49
|
-
"@tanstack/react-table": "^9.1.0",
|
|
50
44
|
"@vitejs/plugin-react": "^6.0.4",
|
|
51
45
|
"@xyflow/react": "^12.11.2",
|
|
52
46
|
"class-variance-authority": "^0.7.1",
|
|
53
47
|
"clsx": "^2.1.1",
|
|
54
48
|
"cmdk": "^1.1.1",
|
|
55
49
|
"colorjs.io": "^0.7.1",
|
|
56
|
-
"croner": "^10.0.1",
|
|
57
50
|
"fflate": "^0.8.3",
|
|
58
51
|
"graphology": "^0.26.0",
|
|
59
52
|
"lucide-react": "^1.25.0",
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
+
import type { NodeKind } from "@anchrd/intel-contract/node";
|
|
1
2
|
import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
|
|
2
3
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
3
|
-
import { zipSync } from "fflate";
|
|
4
4
|
import {
|
|
5
5
|
ChevronRight,
|
|
6
6
|
CornerLeftUp,
|
|
7
7
|
FileArchive,
|
|
8
|
-
FileText,
|
|
9
|
-
Folder,
|
|
10
8
|
FolderOpen,
|
|
11
9
|
FolderUp,
|
|
12
10
|
Plus,
|
|
13
|
-
Table,
|
|
14
11
|
Upload,
|
|
15
|
-
Workflow,
|
|
16
12
|
} from "lucide-react";
|
|
17
13
|
import { useRef, useState } from "react";
|
|
18
|
-
import { defaultModel } from "@/agent/agent-models/agent-models.ts";
|
|
19
14
|
import {
|
|
20
15
|
type MoveDestination,
|
|
21
16
|
moveErrorKey,
|
|
@@ -28,6 +23,7 @@ import {
|
|
|
28
23
|
DropdownMenu,
|
|
29
24
|
DropdownMenuContent,
|
|
30
25
|
DropdownMenuItem,
|
|
26
|
+
DropdownMenuSeparator,
|
|
31
27
|
DropdownMenuTrigger,
|
|
32
28
|
} from "@/components/ui/dropdown-menu";
|
|
33
29
|
import {
|
|
@@ -39,40 +35,27 @@ import {
|
|
|
39
35
|
} from "@/components/ui/sidebar";
|
|
40
36
|
import { flowEntry } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
41
37
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
42
|
-
import { useCapabilities } from "@/hooks/use-capabilities.ts";
|
|
43
38
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
39
|
+
import { type ImportKind, useBundleImport } from "@/import-bundle/import-bundle.tsx";
|
|
44
40
|
import { kindIcons } from "@/kind-icon.ts";
|
|
45
41
|
import { Modal } from "@/modal/modal.tsx";
|
|
46
42
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
47
43
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
/**
|
|
46
|
+
* What the plus can MAKE — derived from `NodeKind`, never written out again (#395).
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ `attachment` is excluded because an attachment is uploaded rather than created, and `flow` is
|
|
49
|
+
* added because a flow shares the tree without being a node (ADR-0004 §1). Both are decisions about
|
|
50
|
+
* this menu; the LIST of node kinds is not, and a copy of it here goes stale in the direction that
|
|
51
|
+
* already bit us — the parked kinds stood in such a union long after nothing could make one.
|
|
52
|
+
*/
|
|
53
|
+
type NewKind = Exclude<NodeKind, "attachment"> | "flow";
|
|
53
54
|
type Creating = { parentId: string | null; kind: NewKind };
|
|
54
55
|
|
|
55
56
|
// Attachments are inlined as base64, so the browser holds the file twice while it uploads.
|
|
56
57
|
const MaxUploadBytes = 15_000_000;
|
|
57
58
|
|
|
58
|
-
// The server refuses anything larger anyway (#137); refusing here saves uploading it first.
|
|
59
|
-
const MaxImportBytes = 64 * 1024 * 1024;
|
|
60
|
-
|
|
61
|
-
// A picked directory as the zip the import endpoint reads. `webkitRelativePath` carries the folder
|
|
62
|
-
// structure, with the picked folder itself as the first segment — kept, so the folder lands as a
|
|
63
|
-
// folder rather than spilling its contents into the target.
|
|
64
|
-
async function zipOfDirectory(files: readonly File[]): Promise<Blob> {
|
|
65
|
-
const entries: Record<string, Uint8Array> = {};
|
|
66
|
-
let total = 0;
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
const path = file.webkitRelativePath || file.name;
|
|
69
|
-
total += file.size;
|
|
70
|
-
if (total > MaxImportBytes) throw new Error("Import exceeds the 64 MB limit");
|
|
71
|
-
entries[path] = new Uint8Array(await file.arrayBuffer());
|
|
72
|
-
}
|
|
73
|
-
return new Blob([zipSync(entries).slice().buffer], { type: "application/zip" });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
59
|
async function fileBase64(file: File): Promise<string> {
|
|
77
60
|
if (file.size > MaxUploadBytes) throw new Error("Attachment exceeds the 15 MB upload limit");
|
|
78
61
|
return await new Promise((resolve, reject) => {
|
|
@@ -115,9 +98,6 @@ export function AppTree() {
|
|
|
115
98
|
const [creating, setCreating] = useState<Creating | null>(null);
|
|
116
99
|
const [uploadTo, setUploadTo] = useState<string | null>(null);
|
|
117
100
|
const uploadInput = useRef<HTMLInputElement>(null);
|
|
118
|
-
const [importTo, setImportTo] = useState<string | null>(null);
|
|
119
|
-
const importZipInput = useRef<HTMLInputElement>(null);
|
|
120
|
-
const importFolderInput = useRef<HTMLInputElement>(null);
|
|
121
101
|
const [dragged, setDragged] = useState<TreeEntry | null>(null);
|
|
122
102
|
const draggedRef = useRef<TreeEntry | null>(null);
|
|
123
103
|
// Which target the pointer is over: `undefined` for none, `null` for the root strip, an id for a
|
|
@@ -131,6 +111,14 @@ export function AppTree() {
|
|
|
131
111
|
if (destination.id !== null) toggle({ id: destination.id, type: "folder" }, true);
|
|
132
112
|
},
|
|
133
113
|
});
|
|
114
|
+
// ⚠️ The root's import, and only the root's. A folder imports through its own three-dot menu
|
|
115
|
+
// (#346); the tree keeps this one because the top level has no resource whose menu could hold it.
|
|
116
|
+
const bundleImport = useBundleImport({
|
|
117
|
+
where: i18n.t("tree.move.root"),
|
|
118
|
+
onImported: (parentId) => {
|
|
119
|
+
if (parentId !== null) toggle({ id: parentId, type: "folder" }, true);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
134
122
|
|
|
135
123
|
const location = useRouterState({
|
|
136
124
|
select: (state) => ({
|
|
@@ -207,26 +195,6 @@ export function AppTree() {
|
|
|
207
195
|
});
|
|
208
196
|
return { area: "/flows" as const, id: flow.id, parentId, collection: "flows" as const };
|
|
209
197
|
}
|
|
210
|
-
// ⚠️ Its own endpoint, not `createNodes`: an agent is created WITH its first definition
|
|
211
|
-
// version, because a definition names the model and there is no agent without one. The
|
|
212
|
-
// create dialog does not ask for it — `defaultModel()` answers, and the profile changes it.
|
|
213
|
-
if (kind === "agent") {
|
|
214
|
-
const created = await data.createAgent({
|
|
215
|
-
parentId,
|
|
216
|
-
title,
|
|
217
|
-
description: null,
|
|
218
|
-
// A new agent has no tools: giving it any is a choice made in the profile, out of what
|
|
219
|
-
// this person reaches in the portal (D30).
|
|
220
|
-
definition: { references: [], schedules: [], model: defaultModel(), tools: null },
|
|
221
|
-
idempotencyKey: crypto.randomUUID(),
|
|
222
|
-
});
|
|
223
|
-
return {
|
|
224
|
-
area: "/nodes" as const,
|
|
225
|
-
id: created.node.id,
|
|
226
|
-
parentId,
|
|
227
|
-
collection: "node-graph" as const,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
198
|
const node = await data.createNodes({
|
|
231
199
|
parentId,
|
|
232
200
|
kind,
|
|
@@ -301,26 +269,6 @@ export function AppTree() {
|
|
|
301
269
|
},
|
|
302
270
|
});
|
|
303
271
|
|
|
304
|
-
// One import, one request, one answer (#137): the server creates the whole subtree or nothing,
|
|
305
|
-
// so this mutation never has a partial state to clean up after — unlike the attachment upload
|
|
306
|
-
// above, whose two steps can strand a node.
|
|
307
|
-
const importBundle = useMutation({
|
|
308
|
-
mutationFn: async ({ parentId, zip }: { parentId: string | null; zip: Blob }) => {
|
|
309
|
-
if (zip.size > MaxImportBytes) throw new Error("Import exceeds the 64 MB limit");
|
|
310
|
-
const result = await data.importNodeBundle({
|
|
311
|
-
nodeId: parentId,
|
|
312
|
-
zip,
|
|
313
|
-
idempotencyKey: crypto.randomUUID(),
|
|
314
|
-
});
|
|
315
|
-
return { parentId, result };
|
|
316
|
-
},
|
|
317
|
-
onSuccess: async (imported) => {
|
|
318
|
-
// A bundle can carry flows, so the flow collections go stale alongside the node ones.
|
|
319
|
-
await reveal(imported.parentId, "node-graph");
|
|
320
|
-
await queryClient.invalidateQueries({ queryKey: ["flows"] });
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
|
|
324
272
|
// What a drop on this target would do. `undefined` means nothing is being dragged, so the target
|
|
325
273
|
// is not a target at all.
|
|
326
274
|
function verdictFor(
|
|
@@ -376,11 +324,6 @@ export function AppTree() {
|
|
|
376
324
|
uploadInput.current?.click();
|
|
377
325
|
}
|
|
378
326
|
|
|
379
|
-
function startImport(parentId: string | null, kind: ImportKind) {
|
|
380
|
-
setImportTo(parentId);
|
|
381
|
-
(kind === "importZip" ? importZipInput : importFolderInput).current?.click();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
327
|
function renderLevel(parent: Level, ancestors: ReadonlySet<string>, label?: string) {
|
|
385
328
|
const level = levelFor(parent);
|
|
386
329
|
if (!level || level.isPending) {
|
|
@@ -566,7 +509,6 @@ export function AppTree() {
|
|
|
566
509
|
label={i18n.t("tree.add", { title: entry.title })}
|
|
567
510
|
onSelect={(kind) => {
|
|
568
511
|
if (kind === "upload") startUpload(target);
|
|
569
|
-
else if (kind === "importZip" || kind === "importFolder") startImport(target, kind);
|
|
570
512
|
else setCreating({ parentId: target, kind });
|
|
571
513
|
}}
|
|
572
514
|
/>
|
|
@@ -621,9 +563,9 @@ export function AppTree() {
|
|
|
621
563
|
{/* The tree is the navigation and carries no heading of its own (ADR-0004); the list is
|
|
622
564
|
named for assistive technology instead. */}
|
|
623
565
|
{renderLevel({ id: null, type: "folder" }, new Set(), i18n.t("tree.label"))}
|
|
624
|
-
{create.isError || upload.isError ||
|
|
566
|
+
{create.isError || upload.isError || bundleImport.isError ? (
|
|
625
567
|
<p role="alert" className="px-2 py-1.5 text-sm text-destructive">
|
|
626
|
-
{
|
|
568
|
+
{bundleImport.isError
|
|
627
569
|
? i18n.t("tree.importFailed")
|
|
628
570
|
: upload.isError
|
|
629
571
|
? i18n.t("tree.uploadFailed")
|
|
@@ -651,7 +593,8 @@ export function AppTree() {
|
|
|
651
593
|
variant="row"
|
|
652
594
|
onSelect={(kind) => {
|
|
653
595
|
if (kind === "upload") startUpload(null);
|
|
654
|
-
else if (kind === "importZip" || kind === "importFolder")
|
|
596
|
+
else if (kind === "importZip" || kind === "importFolder")
|
|
597
|
+
bundleImport.start(null, kind);
|
|
655
598
|
else setCreating({ parentId: null, kind });
|
|
656
599
|
}}
|
|
657
600
|
/>
|
|
@@ -668,38 +611,7 @@ export function AppTree() {
|
|
|
668
611
|
event.currentTarget.value = "";
|
|
669
612
|
}}
|
|
670
613
|
/>
|
|
671
|
-
|
|
672
|
-
ref={importZipInput}
|
|
673
|
-
type="file"
|
|
674
|
-
accept=".zip,application/zip"
|
|
675
|
-
className="sr-only"
|
|
676
|
-
aria-label={i18n.t("tree.new.importZip")}
|
|
677
|
-
onChange={(event) => {
|
|
678
|
-
const file = event.target.files?.[0];
|
|
679
|
-
if (file) importBundle.mutate({ parentId: importTo, zip: file });
|
|
680
|
-
event.currentTarget.value = "";
|
|
681
|
-
}}
|
|
682
|
-
/>
|
|
683
|
-
{/* `webkitdirectory` is the one way a browser hands over a folder; React does not know the
|
|
684
|
-
attribute, so it is spread past the type checker rather than invented as a prop. */}
|
|
685
|
-
<input
|
|
686
|
-
ref={importFolderInput}
|
|
687
|
-
type="file"
|
|
688
|
-
multiple
|
|
689
|
-
{...({ webkitdirectory: "" } as object)}
|
|
690
|
-
className="sr-only"
|
|
691
|
-
aria-label={i18n.t("tree.new.importFolder")}
|
|
692
|
-
onChange={(event) => {
|
|
693
|
-
const files = Array.from(event.target.files ?? []);
|
|
694
|
-
const parentId = importTo;
|
|
695
|
-
if (files.length > 0) {
|
|
696
|
-
void zipOfDirectory(files)
|
|
697
|
-
.then((zip) => importBundle.mutate({ parentId, zip }))
|
|
698
|
-
.catch(() => importBundle.mutate({ parentId, zip: new Blob([]) }));
|
|
699
|
-
}
|
|
700
|
-
event.currentTarget.value = "";
|
|
701
|
-
}}
|
|
702
|
-
/>
|
|
614
|
+
{bundleImport.inputs}
|
|
703
615
|
{creating ? (
|
|
704
616
|
<Modal title={i18n.t(`tree.new.${creating.kind}`)} close={() => setCreating(null)}>
|
|
705
617
|
<CreateForm
|
|
@@ -719,40 +631,27 @@ export function AppTree() {
|
|
|
719
631
|
//
|
|
720
632
|
// `row` is the root's form: a full-width row that says what it does, because the root has no title
|
|
721
633
|
// beside which a bare icon would mean anything.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
label: string;
|
|
728
|
-
variant?: "icon" | "
|
|
729
|
-
|
|
730
|
-
|
|
634
|
+
//
|
|
635
|
+
// ⚠️ The two variants do not offer the same things, and the type says so rather than a comment: only
|
|
636
|
+
// the root can emit an import (#346), because only the root has no three-dot menu to hold one. A
|
|
637
|
+
// single wide signature would leave the folder row with a branch nothing can reach.
|
|
638
|
+
type AddMenuProps =
|
|
639
|
+
| { label: string; variant: "row"; onSelect(kind: NewKind | "upload" | ImportKind): void }
|
|
640
|
+
| { label: string; variant?: "icon"; onSelect(kind: NewKind | "upload"): void };
|
|
641
|
+
|
|
642
|
+
function AddMenu(props: AddMenuProps) {
|
|
643
|
+
const { label, onSelect } = props;
|
|
644
|
+
const variant = props.variant ?? "icon";
|
|
731
645
|
const i18n = useI18n();
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
kind:
|
|
738
|
-
labelKey:
|
|
739
|
-
Icon:
|
|
740
|
-
|
|
741
|
-
{ kind: "folder", labelKey: "tree.new.folder", Icon: Folder },
|
|
742
|
-
{ kind: "document", labelKey: "tree.new.document", Icon: FileText },
|
|
743
|
-
{ kind: "table", labelKey: "tree.new.table", Icon: Table },
|
|
744
|
-
{ kind: "upload", labelKey: "tree.new.upload", Icon: Upload },
|
|
745
|
-
{ kind: "flow", labelKey: "tree.new.flow", Icon: Workflow },
|
|
746
|
-
// ⚠️ The same icon the tree draws for an agent row, taken from the one kind-icon map rather
|
|
747
|
-
// than named again here — a second `Bot` beside it is how the two start disagreeing.
|
|
748
|
-
// Offered only where a runtime exists to run what would be created (#190).
|
|
749
|
-
...(agentRuntime
|
|
750
|
-
? [{ kind: "agent" as const, labelKey: "tree.new.agent", Icon: kindIcons.agent }]
|
|
751
|
-
: []),
|
|
752
|
-
// The bundle round trip's second half (#137): what the export produced, or a plain folder from
|
|
753
|
-
// the machine, lands here as a new subtree.
|
|
754
|
-
{ kind: "importZip", labelKey: "tree.new.importZip", Icon: FileArchive },
|
|
755
|
-
{ kind: "importFolder", labelKey: "tree.new.importFolder", Icon: FolderUp },
|
|
646
|
+
// ⚠️ Three groups, and the separators are the answer to what the plus promises: four kinds are
|
|
647
|
+
// made here, an upload brings one thing in from the machine, an import brings a whole subtree
|
|
648
|
+
// (#346). The icons come from the one kind-icon map rather than being named again — a second
|
|
649
|
+
// `Folder` beside it is how the tree row and this menu start disagreeing.
|
|
650
|
+
const kinds: Array<{ kind: NewKind; labelKey: string; Icon: typeof Plus }> = [
|
|
651
|
+
{ kind: "folder", labelKey: "tree.new.folder", Icon: kindIcons.folder },
|
|
652
|
+
{ kind: "document", labelKey: "tree.new.document", Icon: kindIcons.document },
|
|
653
|
+
{ kind: "table", labelKey: "tree.new.table", Icon: kindIcons.table },
|
|
654
|
+
{ kind: "flow", labelKey: "tree.new.flow", Icon: kindIcons.flow },
|
|
756
655
|
];
|
|
757
656
|
return (
|
|
758
657
|
<DropdownMenu>
|
|
@@ -768,12 +667,34 @@ function AddMenu({
|
|
|
768
667
|
{variant === "row" ? <span className="truncate">{label}</span> : null}
|
|
769
668
|
</DropdownMenuTrigger>
|
|
770
669
|
<DropdownMenuContent align="start" className="w-44">
|
|
771
|
-
{
|
|
670
|
+
{kinds.map((item) => (
|
|
772
671
|
<DropdownMenuItem key={item.kind} onSelect={() => onSelect(item.kind)}>
|
|
773
672
|
<item.Icon aria-hidden="true" />
|
|
774
673
|
{i18n.t(item.labelKey)}
|
|
775
674
|
</DropdownMenuItem>
|
|
776
675
|
))}
|
|
676
|
+
<DropdownMenuSeparator />
|
|
677
|
+
<DropdownMenuItem onSelect={() => onSelect("upload")}>
|
|
678
|
+
<Upload aria-hidden="true" />
|
|
679
|
+
{i18n.t("tree.new.upload")}
|
|
680
|
+
</DropdownMenuItem>
|
|
681
|
+
{/* ⚠️ The root only, and that is not an omission at a folder row: a folder imports through
|
|
682
|
+
its own three-dot menu, beside the export it is the other half of (#136/#137). The root
|
|
683
|
+
has no resource and therefore no such menu, so dropping it here would put the top level
|
|
684
|
+
out of reach of an import altogether (#346). */}
|
|
685
|
+
{props.variant === "row" ? (
|
|
686
|
+
<>
|
|
687
|
+
<DropdownMenuSeparator />
|
|
688
|
+
<DropdownMenuItem onSelect={() => props.onSelect("importZip")}>
|
|
689
|
+
<FileArchive aria-hidden="true" />
|
|
690
|
+
{i18n.t("tree.new.importZip")}
|
|
691
|
+
</DropdownMenuItem>
|
|
692
|
+
<DropdownMenuItem onSelect={() => props.onSelect("importFolder")}>
|
|
693
|
+
<FolderUp aria-hidden="true" />
|
|
694
|
+
{i18n.t("tree.new.importFolder")}
|
|
695
|
+
</DropdownMenuItem>
|
|
696
|
+
</>
|
|
697
|
+
) : null}
|
|
777
698
|
</DropdownMenuContent>
|
|
778
699
|
</DropdownMenu>
|
|
779
700
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Flow
|
|
1
|
+
import type { Flow } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { ToolCapability } from "@anchrd/intel-contract/tool";
|
|
2
3
|
import { useQuery } from "@tanstack/react-query";
|
|
3
4
|
import { useNavigate } from "@tanstack/react-router";
|
|
4
5
|
import { FileText, Search, Workflow, Wrench } from "lucide-react";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
12
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rebuilding the search index, asked for before it happens (#416).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Two clicks, not one, and the reason is not that it is dangerous — nothing is lost, the index is
|
|
18
|
+
* derived from D1 and R2 and can always be built again. It is that it is EXPENSIVE and invisible:
|
|
19
|
+
* every node is read and embedded again, which costs money and minutes, and the screen looks exactly
|
|
20
|
+
* the same afterwards. A one-click button next to "Preferences" invites a second press when the
|
|
21
|
+
* first one seems to have done nothing.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ It reports what was QUEUED, never "done". The answer comes back the moment the work is handed
|
|
24
|
+
* to the queue, and the index catches up behind it — writing "finished" here would be a sentence
|
|
25
|
+
* about something this screen cannot see. The number is the honest part: it says how much was
|
|
26
|
+
* accepted, so a repeat press can be compared against it.
|
|
27
|
+
*
|
|
28
|
+
* The dialog is opened from outside, like the settings one: its trigger is a `DropdownMenuItem`, and
|
|
29
|
+
* Radix unmounts the menu content in the same frame the item is chosen.
|
|
30
|
+
*/
|
|
31
|
+
interface ReindexDialogProps {
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange(open: boolean): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ReindexDialog({ open, onOpenChange }: ReindexDialogProps) {
|
|
37
|
+
const i18n = useI18n();
|
|
38
|
+
const { data } = useIntelRouterContext();
|
|
39
|
+
const reindex = useMutation({ mutationFn: () => data.reindexNodes() });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Dialog
|
|
43
|
+
open={open}
|
|
44
|
+
onOpenChange={(next) => {
|
|
45
|
+
// Closing forgets the last answer, so re-opening never shows a count from an earlier run as
|
|
46
|
+
// if it were this one's.
|
|
47
|
+
if (!next) reindex.reset();
|
|
48
|
+
onOpenChange(next);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<DialogContent className="sm:max-w-md">
|
|
52
|
+
<DialogHeader>
|
|
53
|
+
<DialogTitle>{i18n.t("reindex.title")}</DialogTitle>
|
|
54
|
+
<DialogDescription>{i18n.t("reindex.description")}</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
{reindex.isSuccess ? (
|
|
57
|
+
<p role="status" className="text-sm">
|
|
58
|
+
{i18n.t("reindex.queued", { count: reindex.data.queued })}
|
|
59
|
+
</p>
|
|
60
|
+
) : null}
|
|
61
|
+
{reindex.isError ? (
|
|
62
|
+
<p role="alert" className="text-sm text-destructive">
|
|
63
|
+
{i18n.t("reindex.failed")}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
66
|
+
<DialogFooter>
|
|
67
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
68
|
+
{i18n.t(reindex.isSuccess ? "reindex.close" : "reindex.cancel")}
|
|
69
|
+
</Button>
|
|
70
|
+
{reindex.isSuccess ? null : (
|
|
71
|
+
<Button disabled={reindex.isPending} onClick={() => reindex.mutate()}>
|
|
72
|
+
{i18n.t(reindex.isPending ? "reindex.running" : "reindex.confirm")}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</DialogFooter>
|
|
76
|
+
</DialogContent>
|
|
77
|
+
</Dialog>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -14,16 +14,19 @@ import {
|
|
|
14
14
|
} from "@/components/ui/select";
|
|
15
15
|
import { useI18n, useLanguage } from "@/i18n/i18n-context.tsx";
|
|
16
16
|
import { languageName } from "@/i18n/i18n-languages/i18n-languages.ts";
|
|
17
|
-
import { SectionHint } from "@/section-hint/section-hint.tsx";
|
|
18
17
|
import { type ThemeChoice, ThemeChoices } from "@/theme/theme.ts";
|
|
19
18
|
import { useTheme } from "@/theme/theme-context.tsx";
|
|
20
|
-
import { TimezoneCombobox } from "@/timezone/timezone-combobox/timezone-combobox.tsx";
|
|
21
|
-
import { useTimezoneSelection } from "@/timezone/timezone-context.tsx";
|
|
22
19
|
|
|
23
|
-
// The settings that belong to the person rather than to the installation: language
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
20
|
+
// The settings that belong to the person rather than to the installation: language and appearance.
|
|
21
|
+
// Both are client state — they live in localStorage, not in the Intel HTTP surface — so they do not
|
|
22
|
+
// belong on a screen with a save button. A choice applies at once; a dialog that previews and only
|
|
23
|
+
// commits on "Save" would carry two states too many for two dropdowns.
|
|
24
|
+
//
|
|
25
|
+
// ⚠️ There was a third, the reader's timezone, and it left with the Agents (#388). It was a
|
|
26
|
+
// suggestion for a schedule and the lens its calendar was read through (#228); with both gone
|
|
27
|
+
// nothing read the value, and a preference that changes nothing on screen is worse than none —
|
|
28
|
+
// it invites the reader to set it and then contradicts them. Reading times in a chosen zone is a
|
|
29
|
+
// feature, and it would come back as one.
|
|
27
30
|
//
|
|
28
31
|
// The dialog is opened from outside (the user footer holds the state), because its trigger is a
|
|
29
32
|
// DropdownMenuItem: Radix unmounts the menu content when an item is chosen and would take a dialog
|
|
@@ -37,7 +40,6 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|
|
37
40
|
const i18n = useI18n();
|
|
38
41
|
const language = useLanguage();
|
|
39
42
|
const theme = useTheme();
|
|
40
|
-
const timezone = useTimezoneSelection();
|
|
41
43
|
|
|
42
44
|
return (
|
|
43
45
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -93,30 +95,6 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|
|
93
95
|
</Select>
|
|
94
96
|
</div>
|
|
95
97
|
) : null}
|
|
96
|
-
{timezone ? (
|
|
97
|
-
<div className="grid gap-2">
|
|
98
|
-
{/* ⚠️ Behind the ⓘ (#249), and the SAME component the agent profile uses — three
|
|
99
|
-
lines of prose under a dropdown explained something once and then pushed the
|
|
100
|
-
dialog apart on every open. The `sr-only` copy below is not a duplicate: Radix
|
|
101
|
-
puts a tooltip's content in the accessibility tree only while it is open, so the
|
|
102
|
-
field would silently lose the description `aria-describedby` points at. */}
|
|
103
|
-
<div className="flex items-center gap-1.5">
|
|
104
|
-
<label className="text-sm font-medium" htmlFor="settings-timezone">
|
|
105
|
-
{i18n.t("settings.timezone")}
|
|
106
|
-
</label>
|
|
107
|
-
<SectionHint hint={i18n.t("settings.timezoneHint")} />
|
|
108
|
-
</div>
|
|
109
|
-
<TimezoneCombobox
|
|
110
|
-
id="settings-timezone"
|
|
111
|
-
value={timezone.current}
|
|
112
|
-
onChange={timezone.select}
|
|
113
|
-
describedBy="settings-timezone-hint"
|
|
114
|
-
/>
|
|
115
|
-
<p id="settings-timezone-hint" className="sr-only">
|
|
116
|
-
{i18n.t("settings.timezoneHint")}
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
) : null}
|
|
120
98
|
</div>
|
|
121
99
|
</DialogContent>
|
|
122
100
|
</Dialog>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Archive as ArchiveIcon,
|
|
5
5
|
ChevronsUpDown,
|
|
6
6
|
LogOut,
|
|
7
|
+
RefreshCw,
|
|
7
8
|
Settings,
|
|
8
9
|
User,
|
|
9
10
|
Wrench,
|
|
@@ -20,6 +21,7 @@ import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
|
|
|
20
21
|
import { loginPath } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
21
22
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
22
23
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
24
|
+
import { ReindexDialog } from "../reindex-dialog/reindex-dialog.tsx";
|
|
23
25
|
import { SettingsDialog } from "../settings-dialog/settings-dialog.tsx";
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -41,6 +43,7 @@ export function UserFooter() {
|
|
|
41
43
|
// The state lives here, not in the dialog: its trigger is a DropdownMenuItem, and Radix unmounts
|
|
42
44
|
// the menu content when an item is chosen — a dialog nested in there would go with it.
|
|
43
45
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
46
|
+
const [reindexOpen, setReindexOpen] = useState(false);
|
|
44
47
|
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
45
48
|
const logout = useMutation({
|
|
46
49
|
mutationFn: () => data.logout(),
|
|
@@ -103,6 +106,20 @@ export function UserFooter() {
|
|
|
103
106
|
<Settings aria-hidden="true" />
|
|
104
107
|
{i18n.t("settings.title")}
|
|
105
108
|
</DropdownMenuItem>
|
|
109
|
+
{/* ⚠️ Drawn only for an administrator, and that is a DRAWING decision — `POST
|
|
110
|
+
/nodes/reindex` asks Gate for `intel/admin` itself and would refuse this row's press
|
|
111
|
+
just the same (#416). Hiding it is not the check; it is not offering everybody a
|
|
112
|
+
door they cannot open. Its own separator, because the rows above belong to the
|
|
113
|
+
person and this one belongs to the installation. */}
|
|
114
|
+
{session.data?.isAdmin ? (
|
|
115
|
+
<>
|
|
116
|
+
<DropdownMenuSeparator />
|
|
117
|
+
<DropdownMenuItem onSelect={() => setReindexOpen(true)}>
|
|
118
|
+
<RefreshCw aria-hidden="true" />
|
|
119
|
+
{i18n.t("reindex.title")}
|
|
120
|
+
</DropdownMenuItem>
|
|
121
|
+
</>
|
|
122
|
+
) : null}
|
|
106
123
|
<DropdownMenuSeparator />
|
|
107
124
|
<DropdownMenuItem
|
|
108
125
|
disabled={logout.isPending}
|
|
@@ -116,6 +133,8 @@ export function UserFooter() {
|
|
|
116
133
|
</DropdownMenu>
|
|
117
134
|
{/* ⚠️ Outside the DropdownMenu on purpose — see the state above. */}
|
|
118
135
|
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
136
|
+
{/* ⚠️ Outside the DropdownMenu for the same reason as the settings dialog. */}
|
|
137
|
+
<ReindexDialog open={reindexOpen} onOpenChange={setReindexOpen} />
|
|
119
138
|
</SidebarMenuItem>
|
|
120
139
|
{/* ⚠️ Outside the menu on purpose. Choosing sign-out closes the popup, so a refusal rendered
|
|
121
140
|
inside it would be gone in the same frame it was written — the one message that must
|
package/src/archive/archive.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Flow
|
|
1
|
+
import type { Flow } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { Node } from "@anchrd/intel-contract/node";
|
|
2
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
4
|
import { ArchiveRestore } from "lucide-react";
|
|
4
5
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|