@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
|
@@ -5,28 +5,22 @@ import type {
|
|
|
5
5
|
ResourceRole,
|
|
6
6
|
} from "@anchrd/intel-contract";
|
|
7
7
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
8
9
|
import {
|
|
9
10
|
Archive,
|
|
10
|
-
ChevronDown,
|
|
11
|
-
ChevronRight,
|
|
12
11
|
Download,
|
|
13
|
-
FileText,
|
|
14
|
-
Folder,
|
|
15
|
-
FolderPlus,
|
|
16
12
|
History,
|
|
17
13
|
Link2,
|
|
18
14
|
Network,
|
|
19
15
|
Paperclip,
|
|
20
|
-
Plus,
|
|
21
|
-
Search,
|
|
22
16
|
Share2,
|
|
23
17
|
Trash2,
|
|
24
18
|
} from "lucide-react";
|
|
25
|
-
import { lazy, Suspense,
|
|
26
|
-
import {
|
|
27
|
-
import type { KnowledgeTreeNode } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
19
|
+
import { lazy, Suspense, useState } from "react";
|
|
20
|
+
import { HeaderActions } from "@/app/header-actions/header-actions.tsx";
|
|
28
21
|
import { Modal } from "@/modal/modal.tsx";
|
|
29
22
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
23
|
+
import { selectedFrom } from "@/router/selection-search.ts";
|
|
30
24
|
|
|
31
25
|
const KnowledgeEditor = lazy(async () => ({
|
|
32
26
|
default: (await import("@/knowledge-editor/knowledge-editor.tsx")).KnowledgeEditor,
|
|
@@ -35,65 +29,30 @@ const KnowledgeGraphView = lazy(async () => ({
|
|
|
35
29
|
default: (await import("@/knowledge-graph/knowledge-graph.tsx")).KnowledgeGraphView,
|
|
36
30
|
}));
|
|
37
31
|
|
|
38
|
-
function findNode(nodes: KnowledgeTreeNode[], id: string | null): KnowledgeTreeNode | null {
|
|
39
|
-
if (!id) return null;
|
|
40
|
-
for (const node of nodes) {
|
|
41
|
-
if (node.id === id) return node;
|
|
42
|
-
const child = findNode(node.children, id);
|
|
43
|
-
if (child) return child;
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function flattenTree(nodes: KnowledgeTreeNode[]): KnowledgeTreeNode[] {
|
|
49
|
-
return nodes.flatMap((node) => [node, ...flattenTree(node.children)]);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function fileBase64(file: File): Promise<string> {
|
|
53
|
-
if (file.size > 15_000_000) throw new Error("Attachment exceeds the 15 MB upload limit");
|
|
54
|
-
return await new Promise((resolve, reject) => {
|
|
55
|
-
const reader = new FileReader();
|
|
56
|
-
reader.onerror = () => reject(reader.error ?? new Error("Attachment could not be read"));
|
|
57
|
-
reader.onload = () => {
|
|
58
|
-
const result = reader.result;
|
|
59
|
-
if (typeof result !== "string" || !result.includes(",")) {
|
|
60
|
-
reject(new Error("Attachment could not be encoded"));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
resolve(result.slice(result.indexOf(",") + 1));
|
|
64
|
-
};
|
|
65
|
-
reader.readAsDataURL(file);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
32
|
export function Knowledge() {
|
|
70
33
|
const { data, i18n } = useIntelRouterContext();
|
|
71
34
|
const queryClient = useQueryClient();
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
// The tree in the sidebar and the header search both name what to open through `?select=`, so this
|
|
37
|
+
// screen has no selection of its own to keep in step with them.
|
|
38
|
+
const selectedId = useRouterState({ select: (state) => selectedFrom(state.location.search) });
|
|
75
39
|
const [sharing, setSharing] = useState(false);
|
|
76
40
|
const [linksOpen, setLinksOpen] = useState(false);
|
|
77
41
|
const [graphOpen, setGraphOpen] = useState(false);
|
|
78
42
|
const [versionsOpen, setVersionsOpen] = useState(false);
|
|
79
|
-
const [searchText, setSearchText] = useState("");
|
|
80
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
81
|
-
const selected = useMemo(() => findNode(tree.data ?? [], selectedId), [selectedId, tree.data]);
|
|
82
43
|
const graph = useQuery({
|
|
83
44
|
queryKey: ["knowledge-graph"],
|
|
84
45
|
queryFn: () => data.getKnowledgeGraph(),
|
|
85
|
-
|
|
46
|
+
// The link dialog needs candidates to link to, and the graph is the one authorized list of
|
|
47
|
+
// nodes the UI can ask for without walking the whole tree again.
|
|
48
|
+
enabled: graphOpen || linksOpen,
|
|
86
49
|
});
|
|
87
50
|
const document = useQuery({
|
|
88
51
|
queryKey: ["knowledge", selectedId],
|
|
89
52
|
queryFn: () => data.getKnowledge(selectedId ?? ""),
|
|
90
|
-
enabled: Boolean(selectedId
|
|
91
|
-
});
|
|
92
|
-
const search = useQuery({
|
|
93
|
-
queryKey: ["knowledge-search", searchQuery],
|
|
94
|
-
queryFn: () => data.searchKnowledge({ query: searchQuery, limit: 12 }),
|
|
95
|
-
enabled: searchQuery.length > 0,
|
|
53
|
+
enabled: Boolean(selectedId),
|
|
96
54
|
});
|
|
55
|
+
const selected = document.data?.node ?? null;
|
|
97
56
|
const archive = useMutation({
|
|
98
57
|
mutationFn: (node: KnowledgeNode) =>
|
|
99
58
|
data.archiveKnowledge({
|
|
@@ -102,9 +61,11 @@ export function Knowledge() {
|
|
|
102
61
|
archived: true,
|
|
103
62
|
idempotencyKey: crypto.randomUUID(),
|
|
104
63
|
}),
|
|
105
|
-
onSuccess: async () => {
|
|
106
|
-
|
|
107
|
-
|
|
64
|
+
onSuccess: async (node) => {
|
|
65
|
+
// The row leaves the tree, so the level it sat in is what has to be re-read — and the screen
|
|
66
|
+
// has nothing left to show.
|
|
67
|
+
await queryClient.invalidateQueries({ queryKey: ["tree", node.parentId] });
|
|
68
|
+
await navigate({ to: "/knowledge", search: {} });
|
|
108
69
|
},
|
|
109
70
|
});
|
|
110
71
|
const setContextPolicy = useMutation({
|
|
@@ -115,337 +76,146 @@ export function Knowledge() {
|
|
|
115
76
|
contextPolicy,
|
|
116
77
|
idempotencyKey: crypto.randomUUID(),
|
|
117
78
|
}),
|
|
118
|
-
onSuccess: async () => {
|
|
119
|
-
await queryClient.invalidateQueries({ queryKey: ["knowledge-tree"] });
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
const upload = useMutation({
|
|
123
|
-
mutationFn: async (file: File) => {
|
|
124
|
-
const contentBase64 = await fileBase64(file);
|
|
125
|
-
const node = await data.createKnowledge({
|
|
126
|
-
parentId: selected?.kind === "folder" ? selected.id : (selected?.parentId ?? null),
|
|
127
|
-
kind: "attachment",
|
|
128
|
-
title: file.name,
|
|
129
|
-
description: null,
|
|
130
|
-
contextPolicy: "relevant",
|
|
131
|
-
idempotencyKey: crypto.randomUUID(),
|
|
132
|
-
});
|
|
133
|
-
try {
|
|
134
|
-
const saved = await data.saveKnowledgeAttachment({
|
|
135
|
-
nodeId: node.id,
|
|
136
|
-
baseVersionId: null,
|
|
137
|
-
contentBase64,
|
|
138
|
-
mediaType: file.type || "application/octet-stream",
|
|
139
|
-
idempotencyKey: crypto.randomUUID(),
|
|
140
|
-
});
|
|
141
|
-
return saved.node;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
const current = await data.getKnowledge(node.id).catch(() => null);
|
|
144
|
-
if (current?.version === null && current.node.updatedAt === node.updatedAt) {
|
|
145
|
-
await data
|
|
146
|
-
.archiveKnowledge({
|
|
147
|
-
nodeId: node.id,
|
|
148
|
-
baseUpdatedAt: current.node.updatedAt,
|
|
149
|
-
archived: true,
|
|
150
|
-
idempotencyKey: crypto.randomUUID(),
|
|
151
|
-
})
|
|
152
|
-
.catch(() => undefined);
|
|
153
|
-
}
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
79
|
onSuccess: async (node) => {
|
|
158
|
-
|
|
159
|
-
await queryClient.invalidateQueries({ queryKey: ["knowledge-tree"] });
|
|
80
|
+
await queryClient.invalidateQueries({ queryKey: ["knowledge", node.id] });
|
|
160
81
|
},
|
|
161
82
|
});
|
|
162
83
|
|
|
163
|
-
const renderTreeItem = (node: KnowledgeTreeNode) => (
|
|
164
|
-
<TreeItem
|
|
165
|
-
id={node.id}
|
|
166
|
-
textValue={node.title}
|
|
167
|
-
className="rounded-md outline-none data-[focused]:ring-2 data-[focused]:ring-ring data-[selected]:bg-accent data-[selected]:text-accent-foreground"
|
|
168
|
-
>
|
|
169
|
-
<TreeItemContent>
|
|
170
|
-
{({ hasChildItems, isExpanded }) => (
|
|
171
|
-
<div className="flex min-w-0 items-center gap-2 px-2 py-1.5 text-sm">
|
|
172
|
-
{hasChildItems ? (
|
|
173
|
-
<Button
|
|
174
|
-
slot="chevron"
|
|
175
|
-
className="rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
176
|
-
>
|
|
177
|
-
{isExpanded ? (
|
|
178
|
-
<ChevronDown aria-hidden="true" className="size-3.5" />
|
|
179
|
-
) : (
|
|
180
|
-
<ChevronRight aria-hidden="true" className="size-3.5" />
|
|
181
|
-
)}
|
|
182
|
-
</Button>
|
|
183
|
-
) : (
|
|
184
|
-
<span className="size-3.5" />
|
|
185
|
-
)}
|
|
186
|
-
{node.kind === "folder" ? (
|
|
187
|
-
<Folder aria-hidden="true" className="size-4 shrink-0" />
|
|
188
|
-
) : (
|
|
189
|
-
<FileText aria-hidden="true" className="size-4 shrink-0" />
|
|
190
|
-
)}
|
|
191
|
-
<span className="truncate">{node.title}</span>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
</TreeItemContent>
|
|
195
|
-
<Collection items={node.children}>{renderTreeItem}</Collection>
|
|
196
|
-
</TreeItem>
|
|
197
|
-
);
|
|
198
|
-
|
|
199
84
|
return (
|
|
200
|
-
<div className="flex h-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{i18n.t("knowledge.graph")}
|
|
214
|
-
</button>
|
|
215
|
-
<button
|
|
216
|
-
type="button"
|
|
217
|
-
onClick={() => setCreating("folder")}
|
|
218
|
-
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"
|
|
219
|
-
>
|
|
220
|
-
<FolderPlus aria-hidden="true" className="size-4" />
|
|
221
|
-
{i18n.t("knowledge.newFolder")}
|
|
222
|
-
</button>
|
|
223
|
-
<label className="inline-flex cursor-pointer items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm outline-none hover:bg-muted focus-within:ring-2 focus-within:ring-ring">
|
|
224
|
-
<Paperclip aria-hidden="true" className="size-4" />
|
|
225
|
-
{upload.isPending ? i18n.t("knowledge.uploading") : i18n.t("knowledge.upload")}
|
|
226
|
-
<input
|
|
227
|
-
type="file"
|
|
228
|
-
className="sr-only"
|
|
229
|
-
disabled={upload.isPending}
|
|
230
|
-
onChange={(event) => {
|
|
231
|
-
const file = event.target.files?.[0];
|
|
232
|
-
if (file) upload.mutate(file);
|
|
233
|
-
event.currentTarget.value = "";
|
|
234
|
-
}}
|
|
235
|
-
/>
|
|
236
|
-
</label>
|
|
237
|
-
<button
|
|
238
|
-
type="button"
|
|
239
|
-
onClick={() => setCreating("document")}
|
|
240
|
-
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"
|
|
241
|
-
>
|
|
242
|
-
<Plus aria-hidden="true" className="size-4" />
|
|
243
|
-
{i18n.t("knowledge.new")}
|
|
244
|
-
</button>
|
|
245
|
-
</div>
|
|
246
|
-
</header>
|
|
247
|
-
{upload.isError && (
|
|
248
|
-
<p role="alert" className="border-b px-8 py-3 text-sm text-destructive">
|
|
249
|
-
{i18n.t("knowledge.uploadFailed")}
|
|
250
|
-
</p>
|
|
251
|
-
)}
|
|
85
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
86
|
+
{/* The screen owns the one action that belongs to Knowledge as a whole. Creating things is
|
|
87
|
+
the tree's, in the sidebar, at the place a new thing is meant to go. */}
|
|
88
|
+
<HeaderActions>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => setGraphOpen(true)}
|
|
92
|
+
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"
|
|
93
|
+
>
|
|
94
|
+
<Network aria-hidden="true" className="size-4" />
|
|
95
|
+
{i18n.t("knowledge.graph")}
|
|
96
|
+
</button>
|
|
97
|
+
</HeaderActions>
|
|
252
98
|
{archive.isError || setContextPolicy.isError ? (
|
|
253
99
|
<p role="alert" className="border-b px-8 py-3 text-sm text-destructive">
|
|
254
100
|
{i18n.t("knowledge.operationFailed")}
|
|
255
101
|
</p>
|
|
256
102
|
) : null}
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
>
|
|
266
|
-
<
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
103
|
+
<section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
|
|
104
|
+
{!selectedId ? (
|
|
105
|
+
<div className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
|
|
106
|
+
{i18n.t("knowledge.select")}
|
|
107
|
+
</div>
|
|
108
|
+
) : document.isPending ? (
|
|
109
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
110
|
+
) : document.isError || !selected ? (
|
|
111
|
+
<div role="alert" className="grid flex-1 place-items-center p-8 text-center text-sm">
|
|
112
|
+
<div className="space-y-3">
|
|
113
|
+
<p className="text-destructive">{i18n.t("knowledge.loadFailed")}</p>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => void document.refetch()}
|
|
117
|
+
className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
118
|
+
>
|
|
119
|
+
{i18n.t("common.retry")}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
) : (
|
|
124
|
+
<>
|
|
125
|
+
<div className="flex items-start justify-between gap-5 border-b px-6 py-4">
|
|
126
|
+
<div className="min-w-0">
|
|
127
|
+
<h2 className="truncate text-lg font-semibold">{selected.title}</h2>
|
|
128
|
+
{selected.description && (
|
|
129
|
+
<p className="mt-1 text-sm text-muted-foreground">{selected.description}</p>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
133
|
+
<select
|
|
134
|
+
value={selected.contextPolicy}
|
|
135
|
+
disabled={setContextPolicy.isPending}
|
|
136
|
+
onChange={(event) =>
|
|
137
|
+
setContextPolicy.mutate({
|
|
138
|
+
node: selected,
|
|
139
|
+
contextPolicy: event.target.value as ContextPolicy,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
aria-label={i18n.t("knowledge.contextPolicy")}
|
|
143
|
+
className="mr-2 rounded-md border bg-background px-2 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
144
|
+
>
|
|
145
|
+
<option value="pinned">{i18n.t("knowledge.context.pinned")}</option>
|
|
146
|
+
<option value="relevant">{i18n.t("knowledge.context.relevant")}</option>
|
|
147
|
+
<option value="explicit">{i18n.t("knowledge.context.explicit")}</option>
|
|
148
|
+
</select>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => setLinksOpen(true)}
|
|
152
|
+
aria-label={i18n.t("knowledge.links")}
|
|
153
|
+
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
154
|
+
>
|
|
155
|
+
<Link2 aria-hidden="true" className="size-4" />
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={() => setSharing(true)}
|
|
160
|
+
aria-label={i18n.t("knowledge.share")}
|
|
161
|
+
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
162
|
+
>
|
|
163
|
+
<Share2 aria-hidden="true" className="size-4" />
|
|
164
|
+
</button>
|
|
165
|
+
{selected.kind !== "folder" && (
|
|
285
166
|
<button
|
|
286
|
-
key={`${citation.nodeId}:${citation.versionId}`}
|
|
287
167
|
type="button"
|
|
288
|
-
onClick={() =>
|
|
289
|
-
|
|
168
|
+
onClick={() => setVersionsOpen((value) => !value)}
|
|
169
|
+
aria-label={i18n.t("knowledge.versions")}
|
|
170
|
+
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
290
171
|
>
|
|
291
|
-
<
|
|
292
|
-
<span className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
|
293
|
-
{citation.passage}
|
|
294
|
-
</span>
|
|
172
|
+
<History aria-hidden="true" className="size-4" />
|
|
295
173
|
</button>
|
|
296
|
-
))}
|
|
297
|
-
{search.data?.items.length === 0 && (
|
|
298
|
-
<p className="p-2 text-sm text-muted-foreground">
|
|
299
|
-
{i18n.t("knowledge.noResults")}
|
|
300
|
-
</p>
|
|
301
174
|
)}
|
|
302
175
|
<button
|
|
303
176
|
type="button"
|
|
304
|
-
onClick={() =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}}
|
|
308
|
-
className="mt-3 text-xs text-muted-foreground underline outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
177
|
+
onClick={() => archive.mutate(selected)}
|
|
178
|
+
aria-label={i18n.t("knowledge.archive")}
|
|
179
|
+
className="rounded-md p-2 text-destructive outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
309
180
|
>
|
|
310
|
-
|
|
181
|
+
<Archive aria-hidden="true" className="size-4" />
|
|
311
182
|
</button>
|
|
312
183
|
</div>
|
|
313
|
-
) : tree.isPending ? (
|
|
314
|
-
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
315
|
-
) : (
|
|
316
|
-
<Tree
|
|
317
|
-
aria-label={i18n.t("knowledge.tree")}
|
|
318
|
-
items={tree.data ?? []}
|
|
319
|
-
selectionMode="single"
|
|
320
|
-
selectedKeys={selectedId ? [selectedId] : []}
|
|
321
|
-
onSelectionChange={(keys) => {
|
|
322
|
-
if (keys === "all") return;
|
|
323
|
-
const key = [...keys][0];
|
|
324
|
-
setSelectedId(key === undefined ? null : String(key));
|
|
325
|
-
}}
|
|
326
|
-
className="outline-none"
|
|
327
|
-
renderEmptyState={() => (
|
|
328
|
-
<p className="p-2 text-sm text-muted-foreground">{i18n.t("knowledge.empty")}</p>
|
|
329
|
-
)}
|
|
330
|
-
>
|
|
331
|
-
{renderTreeItem}
|
|
332
|
-
</Tree>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
</aside>
|
|
336
|
-
<section className="relative flex min-w-0 flex-1 flex-col bg-card">
|
|
337
|
-
{!selected && (
|
|
338
|
-
<div className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
|
|
339
|
-
{i18n.t("knowledge.select")}
|
|
340
184
|
</div>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<div className="flex items-start justify-between gap-5 border-b px-6 py-4">
|
|
345
|
-
<div className="min-w-0">
|
|
346
|
-
<h2 className="truncate text-lg font-semibold">{selected.title}</h2>
|
|
347
|
-
{selected.description && (
|
|
348
|
-
<p className="mt-1 text-sm text-muted-foreground">{selected.description}</p>
|
|
349
|
-
)}
|
|
350
|
-
</div>
|
|
351
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
352
|
-
<select
|
|
353
|
-
value={selected.contextPolicy}
|
|
354
|
-
disabled={setContextPolicy.isPending}
|
|
355
|
-
onChange={(event) =>
|
|
356
|
-
setContextPolicy.mutate({
|
|
357
|
-
node: selected,
|
|
358
|
-
contextPolicy: event.target.value as ContextPolicy,
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
aria-label={i18n.t("knowledge.contextPolicy")}
|
|
362
|
-
className="mr-2 rounded-md border bg-background px-2 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
363
|
-
>
|
|
364
|
-
<option value="pinned">{i18n.t("knowledge.context.pinned")}</option>
|
|
365
|
-
<option value="relevant">{i18n.t("knowledge.context.relevant")}</option>
|
|
366
|
-
<option value="explicit">{i18n.t("knowledge.context.explicit")}</option>
|
|
367
|
-
</select>
|
|
368
|
-
<button
|
|
369
|
-
type="button"
|
|
370
|
-
onClick={() => setLinksOpen(true)}
|
|
371
|
-
aria-label={i18n.t("knowledge.links")}
|
|
372
|
-
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
373
|
-
>
|
|
374
|
-
<Link2 aria-hidden="true" className="size-4" />
|
|
375
|
-
</button>
|
|
376
|
-
<button
|
|
377
|
-
type="button"
|
|
378
|
-
onClick={() => setSharing(true)}
|
|
379
|
-
aria-label={i18n.t("knowledge.share")}
|
|
380
|
-
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
381
|
-
>
|
|
382
|
-
<Share2 aria-hidden="true" className="size-4" />
|
|
383
|
-
</button>
|
|
384
|
-
{selected.kind !== "folder" && (
|
|
385
|
-
<button
|
|
386
|
-
type="button"
|
|
387
|
-
onClick={() => setVersionsOpen((value) => !value)}
|
|
388
|
-
aria-label={i18n.t("knowledge.versions")}
|
|
389
|
-
className="rounded-md p-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
390
|
-
>
|
|
391
|
-
<History aria-hidden="true" className="size-4" />
|
|
392
|
-
</button>
|
|
393
|
-
)}
|
|
394
|
-
<button
|
|
395
|
-
type="button"
|
|
396
|
-
onClick={() => archive.mutate(selected)}
|
|
397
|
-
aria-label={i18n.t("knowledge.archive")}
|
|
398
|
-
className="rounded-md p-2 text-destructive outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
399
|
-
>
|
|
400
|
-
<Archive aria-hidden="true" className="size-4" />
|
|
401
|
-
</button>
|
|
402
|
-
</div>
|
|
185
|
+
{selected.kind === "folder" ? (
|
|
186
|
+
<div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
|
|
187
|
+
{i18n.t("knowledge.folderHelp")}
|
|
403
188
|
</div>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
) : null}
|
|
430
|
-
{versionsOpen && selected.kind !== "folder" && (
|
|
431
|
-
<VersionHistory nodeId={selected.id} close={() => setVersionsOpen(false)} />
|
|
432
|
-
)}
|
|
433
|
-
</>
|
|
434
|
-
)}
|
|
435
|
-
</section>
|
|
436
|
-
</div>
|
|
437
|
-
{creating && (
|
|
438
|
-
<CreateKnowledge
|
|
439
|
-
kind={creating}
|
|
440
|
-
parentId={selected?.kind === "folder" ? selected.id : (selected?.parentId ?? null)}
|
|
441
|
-
close={() => setCreating(null)}
|
|
442
|
-
/>
|
|
443
|
-
)}
|
|
189
|
+
) : selected.kind === "attachment" ? (
|
|
190
|
+
<AttachmentPanel node={selected} />
|
|
191
|
+
) : document.data ? (
|
|
192
|
+
<Suspense
|
|
193
|
+
fallback={
|
|
194
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
<KnowledgeEditor
|
|
198
|
+
key={document.data.node.id}
|
|
199
|
+
data={data}
|
|
200
|
+
document={document.data}
|
|
201
|
+
i18n={i18n}
|
|
202
|
+
onSaved={(saved) => {
|
|
203
|
+
queryClient.setQueryData(["knowledge", selected.id], saved);
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
</Suspense>
|
|
207
|
+
) : null}
|
|
208
|
+
{versionsOpen && selected.kind !== "folder" && (
|
|
209
|
+
<VersionHistory nodeId={selected.id} close={() => setVersionsOpen(false)} />
|
|
210
|
+
)}
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
</section>
|
|
444
214
|
{sharing && selected && <ShareKnowledge node={selected} close={() => setSharing(false)} />}
|
|
445
215
|
{linksOpen && selected && (
|
|
446
216
|
<KnowledgeLinks
|
|
447
217
|
node={selected}
|
|
448
|
-
nodes={
|
|
218
|
+
nodes={graph.data?.nodes ?? []}
|
|
449
219
|
close={() => setLinksOpen(false)}
|
|
450
220
|
/>
|
|
451
221
|
)}
|
|
@@ -472,8 +242,8 @@ export function Knowledge() {
|
|
|
472
242
|
i18n={i18n}
|
|
473
243
|
close={() => setGraphOpen(false)}
|
|
474
244
|
select={(nodeId) => {
|
|
475
|
-
setSelectedId(nodeId);
|
|
476
245
|
setGraphOpen(false);
|
|
246
|
+
void navigate({ to: "/knowledge", search: { select: nodeId } });
|
|
477
247
|
}}
|
|
478
248
|
/>
|
|
479
249
|
</Suspense>
|
|
@@ -657,66 +427,6 @@ function KnowledgeLinks({
|
|
|
657
427
|
);
|
|
658
428
|
}
|
|
659
429
|
|
|
660
|
-
function CreateKnowledge({
|
|
661
|
-
kind,
|
|
662
|
-
parentId,
|
|
663
|
-
close,
|
|
664
|
-
}: {
|
|
665
|
-
kind: "document" | "folder";
|
|
666
|
-
parentId: string | null;
|
|
667
|
-
close(): void;
|
|
668
|
-
}) {
|
|
669
|
-
const { data, i18n } = useIntelRouterContext();
|
|
670
|
-
const queryClient = useQueryClient();
|
|
671
|
-
const [title, setTitle] = useState("");
|
|
672
|
-
const mutation = useMutation({
|
|
673
|
-
mutationFn: () =>
|
|
674
|
-
data.createKnowledge({
|
|
675
|
-
parentId,
|
|
676
|
-
kind,
|
|
677
|
-
title,
|
|
678
|
-
description: null,
|
|
679
|
-
contextPolicy: "relevant",
|
|
680
|
-
idempotencyKey: crypto.randomUUID(),
|
|
681
|
-
}),
|
|
682
|
-
onSuccess: async () => {
|
|
683
|
-
await queryClient.invalidateQueries({ queryKey: ["knowledge-tree"] });
|
|
684
|
-
close();
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
return (
|
|
688
|
-
<Modal
|
|
689
|
-
title={kind === "folder" ? i18n.t("knowledge.newFolder") : i18n.t("knowledge.new")}
|
|
690
|
-
close={close}
|
|
691
|
-
>
|
|
692
|
-
<form
|
|
693
|
-
onSubmit={(event) => {
|
|
694
|
-
event.preventDefault();
|
|
695
|
-
mutation.mutate();
|
|
696
|
-
}}
|
|
697
|
-
className="space-y-4"
|
|
698
|
-
>
|
|
699
|
-
<label className="block text-sm font-medium">
|
|
700
|
-
{i18n.t("common.title")}
|
|
701
|
-
<input
|
|
702
|
-
required
|
|
703
|
-
value={title}
|
|
704
|
-
onChange={(event) => setTitle(event.target.value)}
|
|
705
|
-
className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
706
|
-
/>
|
|
707
|
-
</label>
|
|
708
|
-
<button
|
|
709
|
-
type="submit"
|
|
710
|
-
disabled={mutation.isPending}
|
|
711
|
-
className="w-full rounded-md bg-primary px-4 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"
|
|
712
|
-
>
|
|
713
|
-
{i18n.t("common.create")}
|
|
714
|
-
</button>
|
|
715
|
-
</form>
|
|
716
|
-
</Modal>
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
430
|
function ShareKnowledge({ node, close }: { node: KnowledgeNode; close(): void }) {
|
|
721
431
|
const { data, i18n } = useIntelRouterContext();
|
|
722
432
|
const [email, setEmail] = useState("");
|