@anchrd/intel-ui 0.8.7 → 0.9.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/README.md +2 -2
- package/package.json +5 -2
- package/src/agent/agent-avatar/agent-avatar.tsx +34 -0
- package/src/agent/agent-calendar/agent-calendar.tsx +79 -0
- package/src/agent/agent-chat/agent-chat.tsx +116 -0
- package/src/agent/agent-cron/agent-cron.ts +132 -0
- package/src/agent/agent-definition/agent-definition.ts +64 -0
- package/src/agent/agent-entry-title/agent-entry-title.ts +29 -0
- package/src/agent/agent-log/agent-log.tsx +159 -0
- package/src/agent/agent-models/agent-models.ts +63 -0
- package/src/agent/agent-profile/agent-profile.tsx +512 -0
- package/src/agent/agent-state/agent-state.ts +48 -0
- package/src/agent/agent.tsx +361 -0
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +151 -23
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +16 -16
- package/src/app/tree-move/tree-move.tsx +10 -10
- package/src/app/user-footer/user-footer.tsx +7 -1
- package/src/archive/archive.tsx +154 -0
- package/src/components/ui/avatar.tsx +39 -0
- package/src/components/ui/select.tsx +163 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/data/agent-runtime/agent-runtime.ts +93 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +206 -120
- package/src/data/intel-data-provider/intel-data-provider.types.ts +112 -54
- package/src/entry-picker/entry-picker.tsx +53 -17
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +20 -20
- package/src/folder-contents/folder-contents.tsx +7 -7
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/hooks/use-capabilities.ts +22 -0
- package/src/i18n/en.json +156 -64
- package/src/kind-icon.ts +5 -2
- package/src/{knowledge-editor/knowledge-editor.tsx → node-editor/node-editor.tsx} +20 -20
- package/src/{knowledge-graph → node-graph}/graph-notice.tsx +1 -1
- package/src/{knowledge-graph/knowledge-graph.tsx → node-graph/node-graph.tsx} +4 -4
- package/src/{knowledge-table/knowledge-table.tsx → node-table/node-table.tsx} +14 -14
- package/src/{knowledge/knowledge.tsx → nodes/nodes.tsx} +46 -29
- package/src/resource-menu/resource-menu.tsx +105 -62
- package/src/router/router.tsx +17 -5
- package/src/title-row/title-row.tsx +13 -5
- package/vite.config.ts +4 -0
- /package/src/{knowledge-graph/knowledge-graph.ts → node-graph/node-graph.ts} +0 -0
package/src/i18n/en.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$locale": "en-US",
|
|
3
3
|
"app.name": "Intel",
|
|
4
4
|
"nav.primary": "Primary navigation",
|
|
5
|
-
"nav.
|
|
5
|
+
"nav.nodes": "Intelligence",
|
|
6
6
|
"nav.flows": "Flows",
|
|
7
7
|
"nav.tools": "Tools",
|
|
8
8
|
"shell.toggleSidebar": "Collapse or expand the sidebar",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"shell.userMenu": "Account and tools for {name}",
|
|
13
13
|
"search.open": "Search",
|
|
14
14
|
"search.title": "Search Intel",
|
|
15
|
-
"search.description": "Find
|
|
16
|
-
"search.placeholder": "Search
|
|
15
|
+
"search.description": "Find documents, flows, and tools you are allowed to see.",
|
|
16
|
+
"search.placeholder": "Search documents, flows, and tools",
|
|
17
17
|
"search.filter": "Filter by kind",
|
|
18
18
|
"search.all": "Everything",
|
|
19
|
-
"search.kind.
|
|
19
|
+
"search.kind.node": "Intelligence",
|
|
20
20
|
"search.kind.flow": "Flows",
|
|
21
21
|
"search.kind.tool": "Tools",
|
|
22
22
|
"search.hint": "Type to search.",
|
|
23
23
|
"search.searching": "Searching…",
|
|
24
24
|
"search.empty": "Nothing found for “{query}”.",
|
|
25
25
|
"search.failed": "The search could not be completed.",
|
|
26
|
-
"tree.label": "
|
|
26
|
+
"tree.label": "Intelligence and flows",
|
|
27
27
|
"tree.contents": "Inside {title}",
|
|
28
28
|
"tree.calls": "Flows called by {title}",
|
|
29
29
|
"tree.empty": "Nothing here yet.",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"tree.new.table": "New table",
|
|
40
40
|
"tree.new.upload": "Upload file",
|
|
41
41
|
"tree.new.flow": "New flow",
|
|
42
|
+
"tree.new.agent": "New agent",
|
|
42
43
|
"tree.upload": "File to upload",
|
|
43
44
|
"tree.table.columns": "Columns",
|
|
44
45
|
"tree.table.columnsHint": "Separate the column names with commas. They are the header, and every row appended later must match it.",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"tree.kind.document": "Document",
|
|
49
50
|
"tree.kind.attachment": "File",
|
|
50
51
|
"tree.kind.table": "Table",
|
|
52
|
+
"tree.kind.agent": "Agent",
|
|
51
53
|
"tree.kind.flow": "Flow",
|
|
52
54
|
"tree.move.action": "Move to…",
|
|
53
55
|
"tree.move.title": "Move {title}",
|
|
@@ -73,48 +75,63 @@
|
|
|
73
75
|
"resource.rename": "Rename",
|
|
74
76
|
"resource.renameTitle": "Rename {title}",
|
|
75
77
|
"resource.archive": "Archive",
|
|
78
|
+
"resource.export": "Download as zip",
|
|
79
|
+
"tree.new.importZip": "Import zip bundle",
|
|
80
|
+
"tree.new.importFolder": "Import folder",
|
|
81
|
+
"tree.importFailed": "The import was not applied. Nothing was created \u2014 check the bundle and try again.",
|
|
82
|
+
"resource.exportFailed": "The export could not be downloaded. Check your access and try again.",
|
|
83
|
+
"archive.title": "Archive",
|
|
84
|
+
"archive.description": "Everything you archived, newest first. Restoring puts an entry back where it was filed.",
|
|
85
|
+
"archive.restore": "Restore {title}",
|
|
86
|
+
"archive.restoreAction": "Restore",
|
|
87
|
+
"archive.empty": "Nothing is archived.",
|
|
88
|
+
"archive.failed": "The archive could not be read. Check your access and try again.",
|
|
89
|
+
"archive.restoreFailed": "It was not restored. Somebody else may have changed it — reload and try again.",
|
|
90
|
+
"archive.archivedAt": "Archived {when}",
|
|
76
91
|
"resource.conflict": "It was not changed: somebody else changed this entry first. Reload it and try again.",
|
|
77
92
|
"resource.forbidden": "It was not changed: you may not change this entry.",
|
|
78
93
|
"resource.failed": "The change was not saved. Reload the latest version and try again.",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
94
|
+
"node.empty": "Nothing has been added yet.",
|
|
95
|
+
"node.download": "Download file",
|
|
96
|
+
"node.attachmentHelp": "The canonical file is stored privately in Intel. Its AI-readable projection is indexed separately.",
|
|
97
|
+
"node.table.summary": "{rows} rows, {columns} columns",
|
|
98
|
+
"node.table.download": "Download CSV",
|
|
99
|
+
"node.table.empty": "No rows yet. Rows are appended by flows and agents.",
|
|
100
|
+
"node.table.undefined": "This table has no columns yet.",
|
|
101
|
+
"node.select": "Select a document or folder to work with it.",
|
|
102
|
+
"node.folderEmpty": "Nothing in this folder yet. Create something with the plus in the sidebar.",
|
|
103
|
+
"node.folderFailed": "This folder could not be read.",
|
|
104
|
+
"node.changed": "Changed",
|
|
105
|
+
"node.rowActions": "Actions",
|
|
106
|
+
"node.kind.folder": "Folder",
|
|
107
|
+
"node.kind.document": "Document",
|
|
108
|
+
"node.kind.attachment": "Upload",
|
|
109
|
+
"node.kind.table": "Table",
|
|
110
|
+
"node.kind.agent": "Agent",
|
|
111
|
+
"node.kind.flow": "Flow",
|
|
112
|
+
"node.share": "Share",
|
|
113
|
+
"node.shareAction": "Grant access",
|
|
114
|
+
"node.email": "Email address",
|
|
115
|
+
"node.verbs": "What this grant allows",
|
|
116
|
+
"node.verb.read": "Read",
|
|
117
|
+
"node.verb.write": "Write",
|
|
118
|
+
"node.verb.execute": "Run",
|
|
119
|
+
"node.verb.share": "Share",
|
|
120
|
+
"node.verbHint.read": "See everything in here",
|
|
121
|
+
"node.verbHint.write": "Change and add things",
|
|
122
|
+
"node.verbHint.execute": "Start the flows in here",
|
|
123
|
+
"node.verbHint.execute.agent": "Use this agent — its chat, its schedules and its MCP address",
|
|
124
|
+
"node.verbHint.share": "Give others access",
|
|
125
|
+
"node.organization": "Everyone in the organization",
|
|
126
|
+
"node.revokeShare": "Revoke access",
|
|
127
|
+
"node.noGrants": "Nobody outside the owner has access yet.",
|
|
128
|
+
"node.shareFailed": "Access was not changed. Check your permission and try again.",
|
|
129
|
+
"node.shareUnreadable": "Flows in this folder read documents this grant does not cover: {titles}.",
|
|
130
|
+
"node.shareUnreadableMore": "{count} more are out of reach too, and you cannot see them.",
|
|
131
|
+
"node.shareUnreadableHidden": "{count} documents that flows in this folder read are out of reach with this grant. You cannot see them.",
|
|
132
|
+
"node.shareUnreadableHint": "Nothing is blocked. A run of those flows will simply stop at that step for them.",
|
|
133
|
+
"node.versions": "Version history",
|
|
134
|
+
"node.version": "Version {sequence}",
|
|
118
135
|
"view.showGraph": "Show this level as a graph",
|
|
119
136
|
"view.showEditor": "Back to the editor",
|
|
120
137
|
"view.showRuns": "Show this flow's runs",
|
|
@@ -122,24 +139,24 @@
|
|
|
122
139
|
"view.graphTruncated": "{omitted} more left out — this view draws at most {limit}.",
|
|
123
140
|
"view.graphEmpty": "Nothing here that you may see.",
|
|
124
141
|
"view.graphFailed": "The graph could not be loaded.",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
142
|
+
"node.graphZoomIn": "Zoom in",
|
|
143
|
+
"node.graphZoomOut": "Zoom out",
|
|
144
|
+
"node.graphReset": "Fit graph",
|
|
145
|
+
"node.operationFailed": "The change was not saved. Reload the latest version and try again.",
|
|
146
|
+
"node.loadFailed": "This item could not be loaded. Check your access and try again.",
|
|
147
|
+
"node.downloadFailed": "The attachment could not be downloaded. Check your access and try again.",
|
|
148
|
+
"node.links": "Links and backlinks",
|
|
149
|
+
"node.noLinks": "No explicit relationships yet.",
|
|
150
|
+
"node.linksHelp": "Relationships are written in the text. Link a document from the editor with the / command; this list shows the result and who points here.",
|
|
151
|
+
"node.linkOutgoing": "This document links to it",
|
|
152
|
+
"node.linkIncoming": "It links to this document",
|
|
153
|
+
"node.link.insert": "Link a document",
|
|
154
|
+
"node.link.group": "Documents",
|
|
155
|
+
"node.link.search": "Search documents you can see",
|
|
156
|
+
"node.link.noMatches": "No document of yours matches.",
|
|
157
|
+
"node.link.unresolved": "Unavailable document",
|
|
158
|
+
"node.saveConflict": "This document changed elsewhere. Reload it before saving again.",
|
|
159
|
+
"node.saveError": "This document could not be saved. Reload and try again.",
|
|
143
160
|
"tools.signingIn": "Signing you in to the company portal…",
|
|
144
161
|
"tools.noAccess": "No access to the company portal",
|
|
145
162
|
"tools.noAccessHelp": "Your Intel account does not reach the company MCP portal, so there is nothing to show here. An administrator decides in the portal who may use which server; ask them to include you.",
|
|
@@ -175,7 +192,7 @@
|
|
|
175
192
|
"flows.problem.flow_not_published": "The flow is not published",
|
|
176
193
|
"flows.problem.flow_graph_invalid": "The graph cannot be run",
|
|
177
194
|
"flows.problem.flow_tools_unavailable": "A tool is out of reach",
|
|
178
|
-
"flows.problem.
|
|
195
|
+
"flows.problem.flow_node_forbidden": "Material a step reads is out of reach",
|
|
179
196
|
"flows.problem.flow_subflow_unavailable": "A called flow is unavailable",
|
|
180
197
|
"flows.problem.flow_subflow_not_published": "A called flow is not published",
|
|
181
198
|
"flows.addNode": "Add a step",
|
|
@@ -228,7 +245,7 @@
|
|
|
228
245
|
"flows.operationFailed": "The flow operation failed. Reload the latest version and try again.",
|
|
229
246
|
"flows.needs": "What this flow needs",
|
|
230
247
|
"flows.needsHint": "Read out of the graph. Whether a given person may reach any of it is decided when they run it.",
|
|
231
|
-
"flows.
|
|
248
|
+
"flows.needsNodes": "Documents",
|
|
232
249
|
"flows.needsTools": "Tools",
|
|
233
250
|
"flows.needsHidden": "{count} more you cannot see",
|
|
234
251
|
"flows.needsEmpty": "This flow reads no documents and calls no tools.",
|
|
@@ -257,6 +274,81 @@
|
|
|
257
274
|
"runs.trigger.subflow": "called by another flow",
|
|
258
275
|
"runs.outcome.completed": "done",
|
|
259
276
|
"runs.outcome.failed": "failed",
|
|
277
|
+
"agent.tabs": "Agent views",
|
|
278
|
+
"agent.tab.profile": "Profile",
|
|
279
|
+
"agent.tab.chat": "Chat",
|
|
280
|
+
"agent.tab.calendar": "Calendar",
|
|
281
|
+
"agent.tab.log": "Log",
|
|
282
|
+
"agent.statusRunning": "Running",
|
|
283
|
+
"agent.statusIdle": "Idle",
|
|
284
|
+
"agent.statusPaused": "Paused",
|
|
285
|
+
"agent.statusUnknown": "Runtime not reachable",
|
|
286
|
+
"agent.nextRun": "next run {when} UTC",
|
|
287
|
+
"agent.noNextRun": "no scheduled run",
|
|
288
|
+
"agent.pausedNoRuns": "no schedule fires, no task and no mail is taken; the chat still answers",
|
|
289
|
+
"agent.pause": "Pause",
|
|
290
|
+
"agent.resume": "Resume",
|
|
291
|
+
"agent.runNow": "Run now",
|
|
292
|
+
"agent.runNowNoTarget": "This agent has no schedule, so there is nothing to run.",
|
|
293
|
+
"agent.runNowPaused": "Resume the agent before running it.",
|
|
294
|
+
"agent.actionFailed": "That did not work. Try again.",
|
|
295
|
+
"agent.notPermitted": "You may read this agent, but not drive it. Driving an agent needs the “run agents” permission.",
|
|
296
|
+
"agent.noRuntime": "This installation runs no agent runtime, so this agent cannot chat or run here. Its definition is kept and works again on an installation that has one.",
|
|
297
|
+
"agent.loadFailed": "This agent could not be loaded. Check your access and try again.",
|
|
298
|
+
"agent.saveFailed": "The change was not saved. Somebody else may have changed this agent first — reload and try again.",
|
|
299
|
+
"agent.noDefinition": "This agent has no definition yet, so there is nothing to change here.",
|
|
300
|
+
"agent.contact": "Contact",
|
|
301
|
+
"agent.contactHint": "How this agent is reached. The built-in chat is part of the runtime and is always there.",
|
|
302
|
+
"agent.contactChat": "Built-in chat",
|
|
303
|
+
"agent.contactChatBuiltIn": "Built in, always there",
|
|
304
|
+
"agent.contactMcp": "MCP address",
|
|
305
|
+
"agent.copy": "Copy",
|
|
306
|
+
"agent.copied": "Copied",
|
|
307
|
+
"agent.contactAdd": "Add contact",
|
|
308
|
+
"agent.contactAddReason": "A mail address belongs to the installation, not to the agent's definition, so it is configured where the runtime is deployed.",
|
|
309
|
+
"agent.knows": "What it knows",
|
|
310
|
+
"agent.knowsHint": "The folders this agent reads, and what each one counts as. A system message instructs it, semantic context is searched, memory is written back to.",
|
|
311
|
+
"agent.knowsEmpty": "This agent reads nothing yet.",
|
|
312
|
+
"agent.addReference": "Add folder",
|
|
313
|
+
"agent.removeReferenceOf": "Stop reading {title}",
|
|
314
|
+
"agent.pickFolder": "Folder to read",
|
|
315
|
+
"agent.roleFor": "What {title} counts as",
|
|
316
|
+
"agent.role.system-message": "System message",
|
|
317
|
+
"agent.role.semantic-context": "Semantic context",
|
|
318
|
+
"agent.role.memory": "Memory",
|
|
319
|
+
"agent.unknownEntry": "Not available to you",
|
|
320
|
+
"agent.tools": "Tools",
|
|
321
|
+
"agent.toolsHint": "Read live from the company portal with your own access. Intel stores no tool permissions; the portal decides who may reach what.",
|
|
322
|
+
"agent.schedule": "Schedule",
|
|
323
|
+
"agent.scheduleHint": "When this agent acts on its own, and what it runs. Times are UTC — a definition carries no timezone.",
|
|
324
|
+
"agent.scheduleEmpty": "This agent runs only when it is asked.",
|
|
325
|
+
"agent.addSchedule": "Add schedule",
|
|
326
|
+
"agent.removeSchedule": "Remove this schedule",
|
|
327
|
+
"agent.scheduleTarget": "What to run",
|
|
328
|
+
"agent.cron": "When",
|
|
329
|
+
"agent.cronHint": "Five fields, in UTC: minute, hour, day of month, month, day of week. “0 8 * * *” is every day at 08:00.",
|
|
330
|
+
"agent.model": "Model",
|
|
331
|
+
"agent.modelHint": "Which model does the thinking. A change is picked up by the runtime within a minute.",
|
|
332
|
+
"agent.calendar": "Upcoming runs",
|
|
333
|
+
"agent.calendarUtc": "All times are UTC, because that is when the agent runs.",
|
|
334
|
+
"agent.calendarUnreadable": "None of this agent's schedules can be read as a cron expression, so nothing is planned.",
|
|
335
|
+
"agent.log": "Runs",
|
|
336
|
+
"agent.logEmpty": "This agent has not run yet.",
|
|
337
|
+
"agent.logFailed": "The runs could not be read. The agent runtime may be unreachable from here.",
|
|
338
|
+
"agent.runFailed": "This run could not be read.",
|
|
339
|
+
"agent.runLog": "What this run did",
|
|
340
|
+
"agent.runLogEmpty": "This run recorded nothing.",
|
|
341
|
+
"agent.steps": "{count} steps",
|
|
342
|
+
"agent.trigger.chat": "from the chat",
|
|
343
|
+
"agent.trigger.schedule": "on schedule",
|
|
344
|
+
"agent.trigger.mail": "from an email",
|
|
345
|
+
"agent.trigger.task": "asked over MCP",
|
|
346
|
+
"agent.trigger.manual": "started by hand",
|
|
347
|
+
"agent.chatEmpty": "Write to this agent. It answers with what it knows and the tools it may reach.",
|
|
348
|
+
"agent.chatInput": "Message to this agent",
|
|
349
|
+
"agent.chatPlaceholder": "Ask something…",
|
|
350
|
+
"agent.chatSend": "Send",
|
|
351
|
+
"agent.chatScrollDown": "Jump to the latest message",
|
|
260
352
|
"auth.signOut": "Sign out",
|
|
261
353
|
"auth.signOutFailed": "Signing out failed. Check your connection and try again.",
|
|
262
354
|
"signIn.refusedTitle": "Signed in, but not accepted",
|
package/src/kind-icon.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileText, Folder, type LucideIcon, Paperclip, Table, Workflow } from "lucide-react";
|
|
1
|
+
import { Bot, FileText, Folder, type LucideIcon, Paperclip, Table, Workflow } from "lucide-react";
|
|
2
2
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -8,12 +8,15 @@ import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.t
|
|
|
8
8
|
* draw it the same. A second map beside this one drifts the first time a kind is added: the tree
|
|
9
9
|
* would show the new icon and the folder screen the old one, for the same thing on the same screen.
|
|
10
10
|
* The picture is also only half the answer; both places still name the kind for a reader who cannot
|
|
11
|
-
* see it (`tree.kind.*`, `
|
|
11
|
+
* see it (`tree.kind.*`, `node.kind.*`), because an icon has no text.
|
|
12
12
|
*/
|
|
13
13
|
export const kindIcons: Record<TreeEntry["kind"], LucideIcon> = {
|
|
14
14
|
folder: Folder,
|
|
15
15
|
document: FileText,
|
|
16
16
|
attachment: Paperclip,
|
|
17
17
|
table: Table,
|
|
18
|
+
// An agent is listed and named like any other row (#139), and since #143 opening one leads to
|
|
19
|
+
// its own screen. The sidebar's create menu draws this same icon rather than naming `Bot` again.
|
|
20
|
+
agent: Bot,
|
|
18
21
|
flow: Workflow,
|
|
19
22
|
};
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
BlockNoteDocument,
|
|
3
3
|
BlockNoteMediaType,
|
|
4
4
|
DocumentLinkInlineType,
|
|
5
|
-
type
|
|
6
|
-
type
|
|
5
|
+
type Node,
|
|
6
|
+
type NodeDocument,
|
|
7
7
|
} from "@anchrd/intel-contract";
|
|
8
8
|
import { filterSuggestionItems } from "@blocknote/core";
|
|
9
9
|
import { SuggestionMenuController, useCreateBlockNote } from "@blocknote/react";
|
|
@@ -56,16 +56,16 @@ function editorSnapshot(blocks: unknown): string {
|
|
|
56
56
|
return JSON.stringify(blocks);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function
|
|
59
|
+
export function NodeEditor({
|
|
60
60
|
data,
|
|
61
61
|
document,
|
|
62
62
|
i18n,
|
|
63
63
|
onSaved,
|
|
64
64
|
}: {
|
|
65
65
|
data: IntelDataProvider;
|
|
66
|
-
document:
|
|
66
|
+
document: NodeDocument;
|
|
67
67
|
i18n: I18n;
|
|
68
|
-
onSaved(document:
|
|
68
|
+
onSaved(document: NodeDocument): void;
|
|
69
69
|
}) {
|
|
70
70
|
const initialBlocks = useMemo(() => storedBlocks(document.content), [document.content]);
|
|
71
71
|
// ⚠️ `initialContent` is always passed, never spread in conditionally: a conditional spread makes
|
|
@@ -94,8 +94,8 @@ export function KnowledgeEditor({
|
|
|
94
94
|
const resolved = useQuery({
|
|
95
95
|
// The key is the set of targets: two documents linking to the same thing share one answer, and
|
|
96
96
|
// adding a link asks only because the set changed.
|
|
97
|
-
queryKey: ["
|
|
98
|
-
queryFn: () => data.
|
|
97
|
+
queryKey: ["node-link-titles", [...linkedIds].sort()],
|
|
98
|
+
queryFn: () => data.resolveNodeLinks({ nodeIds: linkedIds }),
|
|
99
99
|
enabled: linkedIds.length > 0,
|
|
100
100
|
});
|
|
101
101
|
// One place decides whether there is anything to save, so the answer cannot differ between the
|
|
@@ -113,7 +113,7 @@ export function KnowledgeEditor({
|
|
|
113
113
|
blocks,
|
|
114
114
|
markdown: await editor.blocksToMarkdownLossy(blocks),
|
|
115
115
|
});
|
|
116
|
-
const node = await data.
|
|
116
|
+
const node = await data.saveNode({
|
|
117
117
|
nodeId: document.node.id,
|
|
118
118
|
baseVersionId: document.version?.id ?? null,
|
|
119
119
|
content: JSON.stringify(payload),
|
|
@@ -167,7 +167,7 @@ export function KnowledgeEditor({
|
|
|
167
167
|
<UnsavedChangesGuard dirty={dirty} />
|
|
168
168
|
{save.isError && (
|
|
169
169
|
<p role="alert" className="mx-6 mt-4 text-sm text-destructive">
|
|
170
|
-
{i18n.t("
|
|
170
|
+
{i18n.t("node.saveError")}
|
|
171
171
|
</p>
|
|
172
172
|
)}
|
|
173
173
|
<div className="min-h-0 flex-1 overflow-y-auto py-6">
|
|
@@ -175,7 +175,7 @@ export function KnowledgeEditor({
|
|
|
175
175
|
value={{
|
|
176
176
|
titles: new Map((resolved.data?.items ?? []).map((item) => [item.nodeId, item.title])),
|
|
177
177
|
pending: linkedIds.length > 0 && resolved.isPending,
|
|
178
|
-
unresolvedLabel: i18n.t("
|
|
178
|
+
unresolvedLabel: i18n.t("node.link.unresolved"),
|
|
179
179
|
}}
|
|
180
180
|
>
|
|
181
181
|
<BlockNoteView
|
|
@@ -194,8 +194,8 @@ export function KnowledgeEditor({
|
|
|
194
194
|
[
|
|
195
195
|
...defaultSlashMenuItems(editor),
|
|
196
196
|
{
|
|
197
|
-
title: i18n.t("
|
|
198
|
-
group: i18n.t("
|
|
197
|
+
title: i18n.t("node.link.insert"),
|
|
198
|
+
group: i18n.t("node.link.group"),
|
|
199
199
|
icon: <Link2 aria-hidden="true" className="size-4" />,
|
|
200
200
|
onItemClick: () => setPicking(true),
|
|
201
201
|
},
|
|
@@ -242,18 +242,18 @@ function DocumentPicker({
|
|
|
242
242
|
}) {
|
|
243
243
|
const [query, setQuery] = useState("");
|
|
244
244
|
const graph = useQuery({
|
|
245
|
-
queryKey: ["
|
|
246
|
-
queryFn: () => data.
|
|
245
|
+
queryKey: ["node-graph"],
|
|
246
|
+
queryFn: () => data.getNodeGraph(),
|
|
247
247
|
});
|
|
248
248
|
const candidates = (graph.data?.nodes ?? [])
|
|
249
|
-
.filter((node:
|
|
250
|
-
.filter((node:
|
|
249
|
+
.filter((node: Node) => node.id !== exclude && node.kind !== "folder")
|
|
250
|
+
.filter((node: Node) => node.title.toLowerCase().includes(query.trim().toLowerCase()))
|
|
251
251
|
.slice(0, 50);
|
|
252
252
|
|
|
253
253
|
return (
|
|
254
|
-
<Modal title={i18n.t("
|
|
254
|
+
<Modal title={i18n.t("node.link.insert")} close={close}>
|
|
255
255
|
<label className="block text-sm font-medium">
|
|
256
|
-
{i18n.t("
|
|
256
|
+
{i18n.t("node.link.search")}
|
|
257
257
|
<input
|
|
258
258
|
// biome-ignore lint/a11y/noAutofocus: the dialog opens from a keystroke to take this term
|
|
259
259
|
autoFocus
|
|
@@ -266,7 +266,7 @@ function DocumentPicker({
|
|
|
266
266
|
{graph.isPending ? (
|
|
267
267
|
<li className="text-sm text-muted-foreground">{i18n.t("common.loading")}</li>
|
|
268
268
|
) : null}
|
|
269
|
-
{candidates.map((node:
|
|
269
|
+
{candidates.map((node: Node) => (
|
|
270
270
|
<li key={node.id}>
|
|
271
271
|
<button
|
|
272
272
|
type="button"
|
|
@@ -278,7 +278,7 @@ function DocumentPicker({
|
|
|
278
278
|
</li>
|
|
279
279
|
))}
|
|
280
280
|
{!graph.isPending && candidates.length === 0 ? (
|
|
281
|
-
<li className="text-sm text-muted-foreground">{i18n.t("
|
|
281
|
+
<li className="text-sm text-muted-foreground">{i18n.t("node.link.noMatches")}</li>
|
|
282
282
|
) : null}
|
|
283
283
|
</ul>
|
|
284
284
|
</Modal>
|
|
@@ -8,7 +8,7 @@ import type { I18n } from "@/i18n/i18n.types.ts";
|
|
|
8
8
|
* see" — permanently, above every graph. Three nodes are three nodes; the reader sees them. What
|
|
9
9
|
* they cannot see is what the view stopped short of, and that is what stayed.
|
|
10
10
|
*
|
|
11
|
-
* Its own file because `
|
|
11
|
+
* Its own file because `node-graph.tsx` pulls Sigma in at module level, and Sigma needs WebGL:
|
|
12
12
|
* a case about this line could not render there at all.
|
|
13
13
|
*/
|
|
14
14
|
export function GraphNotice({ data, i18n }: { data: RelationGraph; i18n: I18n }) {
|
|
@@ -4,7 +4,7 @@ import { LocateFixed, Minus, Plus } from "lucide-react";
|
|
|
4
4
|
import { useEffect, useMemo } from "react";
|
|
5
5
|
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
6
6
|
import { GraphNotice } from "./graph-notice.tsx";
|
|
7
|
-
import { createRelationGraph, resolveGraphColor } from "./
|
|
7
|
+
import { createRelationGraph, resolveGraphColor } from "./node-graph.ts";
|
|
8
8
|
|
|
9
9
|
function labelColors() {
|
|
10
10
|
const label = resolveGraphColor("--foreground");
|
|
@@ -27,17 +27,17 @@ function GraphControls({ i18n }: { i18n: I18n }) {
|
|
|
27
27
|
const camera = useCamera({ duration: 180, factor: 1.5 });
|
|
28
28
|
const controls = [
|
|
29
29
|
{
|
|
30
|
-
label: i18n.t("
|
|
30
|
+
label: i18n.t("node.graphZoomIn"),
|
|
31
31
|
action: camera.zoomIn,
|
|
32
32
|
icon: Plus,
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
|
-
label: i18n.t("
|
|
35
|
+
label: i18n.t("node.graphZoomOut"),
|
|
36
36
|
action: camera.zoomOut,
|
|
37
37
|
icon: Minus,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
|
-
label: i18n.t("
|
|
40
|
+
label: i18n.t("node.graphReset"),
|
|
41
41
|
action: camera.reset,
|
|
42
42
|
icon: LocateFixed,
|
|
43
43
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Node } from "@anchrd/intel-contract";
|
|
2
2
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { Download } from "lucide-react";
|
|
4
4
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
@@ -8,24 +8,24 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
8
8
|
* A table as a grid (#40).
|
|
9
9
|
*
|
|
10
10
|
* Reading is the whole of the first cut: there is no cell editor, and rows arrive through
|
|
11
|
-
* `
|
|
11
|
+
* `node_table_append` — the point of the kind is that an agent fills it without a person
|
|
12
12
|
* retyping anything.
|
|
13
13
|
*
|
|
14
14
|
* ⚠️ The rows are the server's answer, parsed by the server's one CSV reader. A second reader here
|
|
15
15
|
* would eventually disagree with it about a quoted comma, and the grid would then show something
|
|
16
16
|
* the export does not contain.
|
|
17
17
|
*/
|
|
18
|
-
export function
|
|
18
|
+
export function NodeTablePanel({ node }: { node: Node }) {
|
|
19
19
|
const { data, i18n } = useIntelRouterContext();
|
|
20
20
|
const table = useQuery({
|
|
21
|
-
queryKey: ["
|
|
22
|
-
queryFn: () => data.
|
|
21
|
+
queryKey: ["node-table", node.id],
|
|
22
|
+
queryFn: () => data.getNodeTable(node.id),
|
|
23
23
|
});
|
|
24
24
|
// CSV is the export. The canonical body is already CSV, so downloading is handing over the bytes
|
|
25
25
|
// that are stored rather than rendering a second format that could drift from them.
|
|
26
26
|
const download = useMutation({
|
|
27
27
|
mutationFn: async () => {
|
|
28
|
-
const document_ = await data.
|
|
28
|
+
const document_ = await data.getNode(node.id);
|
|
29
29
|
return new Blob([document_.content ?? ""], { type: "text/csv;charset=utf-8" });
|
|
30
30
|
},
|
|
31
31
|
onSuccess: (blob) => {
|
|
@@ -46,13 +46,13 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
46
46
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
47
47
|
{/* ⚠️ Both of these used to be a second bar of their own, directly under the title line (#54):
|
|
48
48
|
two headers stacked, and the table starting a whole row lower for it. A table is one
|
|
49
|
-
|
|
49
|
+
node kind among four, not a screen with its own chrome — so the count goes beside the
|
|
50
50
|
title as a quiet word and the export joins the buttons in the title line. They are rendered
|
|
51
51
|
from here rather than from the screen because this is where the query lives; the screen
|
|
52
52
|
would otherwise have to load a table it does not show. */}
|
|
53
53
|
<ActionSlot name="title-meta">
|
|
54
54
|
{table.data
|
|
55
|
-
? i18n.t("
|
|
55
|
+
? i18n.t("node.table.summary", {
|
|
56
56
|
rows: table.data.rows.length,
|
|
57
57
|
columns: table.data.columns.length,
|
|
58
58
|
})
|
|
@@ -68,7 +68,7 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
68
68
|
className="inline-flex h-8 items-center gap-2 rounded-md border px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
69
69
|
>
|
|
70
70
|
<Download aria-hidden="true" className="size-4" />
|
|
71
|
-
{i18n.t("
|
|
71
|
+
{i18n.t("node.table.download")}
|
|
72
72
|
</button>
|
|
73
73
|
</ActionSlot>
|
|
74
74
|
{/* ⚠️ The refusal stays down here, in the body, where a sentence has room to be read. The
|
|
@@ -76,14 +76,14 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
76
76
|
failure nobody is told about. */}
|
|
77
77
|
{download.isError ? (
|
|
78
78
|
<p role="alert" className="mx-6 mt-4 text-sm text-destructive">
|
|
79
|
-
{i18n.t("
|
|
79
|
+
{i18n.t("node.downloadFailed")}
|
|
80
80
|
</p>
|
|
81
81
|
) : null}
|
|
82
82
|
{table.isPending ? (
|
|
83
83
|
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
84
84
|
) : table.isError ? (
|
|
85
85
|
<div role="alert" className="space-y-3 p-6 text-sm">
|
|
86
|
-
<p className="text-destructive">{i18n.t("
|
|
86
|
+
<p className="text-destructive">{i18n.t("node.loadFailed")}</p>
|
|
87
87
|
<button
|
|
88
88
|
type="button"
|
|
89
89
|
onClick={() => void table.refetch()}
|
|
@@ -94,7 +94,7 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
94
94
|
</div>
|
|
95
95
|
) : table.data.columns.length === 0 ? (
|
|
96
96
|
<p className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
|
|
97
|
-
{i18n.t("
|
|
97
|
+
{i18n.t("node.table.undefined")}
|
|
98
98
|
</p>
|
|
99
99
|
) : (
|
|
100
100
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
@@ -103,7 +103,7 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
103
103
|
<thead className="sticky top-0 bg-card">
|
|
104
104
|
<tr>
|
|
105
105
|
{table.data.columns.map((column) => (
|
|
106
|
-
// Column names are distinct by contract (`
|
|
106
|
+
// Column names are distinct by contract (`DefineTableInput`), so the
|
|
107
107
|
// name is the key and nothing has to fall back to a position.
|
|
108
108
|
<th
|
|
109
109
|
key={column}
|
|
@@ -132,7 +132,7 @@ export function KnowledgeTablePanel({ node }: { node: KnowledgeNode }) {
|
|
|
132
132
|
</tbody>
|
|
133
133
|
</table>
|
|
134
134
|
{table.data.rows.length === 0 ? (
|
|
135
|
-
<p className="p-6 text-sm text-muted-foreground">{i18n.t("
|
|
135
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("node.table.empty")}</p>
|
|
136
136
|
) : null}
|
|
137
137
|
</div>
|
|
138
138
|
)}
|