@schlessera/brain-ui-react 0.34.0 → 0.35.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 +12 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +8 -149
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/activity-rollups.d.ts +5 -0
- package/dist/components/activity/activity-rollups.d.ts.map +1 -0
- package/dist/components/activity/activity-rollups.js +39 -0
- package/dist/components/activity/activity-rollups.js.map +1 -0
- package/dist/components/activity/activity-run-detail.d.ts +11 -0
- package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
- package/dist/components/activity/activity-run-detail.js +111 -0
- package/dist/components/activity/activity-run-detail.js.map +1 -0
- package/dist/components/activity/activity-run-list.d.ts +15 -0
- package/dist/components/activity/activity-run-list.d.ts.map +1 -0
- package/dist/components/activity/activity-run-list.js +25 -0
- package/dist/components/activity/activity-run-list.js.map +1 -0
- package/dist/components/chat/brain-markdown-code.d.ts +4 -0
- package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-code.js +47 -0
- package/dist/components/chat/brain-markdown-code.js.map +1 -0
- package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
- package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-entities.js +93 -0
- package/dist/components/chat/brain-markdown-entities.js.map +1 -0
- package/dist/components/chat/brain-markdown-links.d.ts +36 -0
- package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-links.js +176 -0
- package/dist/components/chat/brain-markdown-links.js.map +1 -0
- package/dist/components/chat/brain-markdown-share.d.ts +15 -0
- package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-share.js +40 -0
- package/dist/components/chat/brain-markdown-share.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +1 -34
- package/dist/components/chat/brain-markdown.d.ts.map +1 -1
- package/dist/components/chat/brain-markdown.js +9 -402
- package/dist/components/chat/brain-markdown.js.map +1 -1
- package/dist/components/chat/composer.d.ts.map +1 -1
- package/dist/components/chat/composer.js +14 -1
- package/dist/components/chat/composer.js.map +1 -1
- package/dist/components/chat/session-drawer.d.ts.map +1 -1
- package/dist/components/chat/session-drawer.js +25 -10
- package/dist/components/chat/session-drawer.js.map +1 -1
- package/dist/components/chat/tool-views.d.ts +2 -2
- package/dist/components/chat/tool-views.d.ts.map +1 -1
- package/dist/components/chat/tool-views.js +2 -16
- package/dist/components/chat/tool-views.js.map +1 -1
- package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
- package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
- package/dist/components/chat/use-markdown-highlight.js +56 -0
- package/dist/components/chat/use-markdown-highlight.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
- package/dist/components/connectivity/connection-gate.js +94 -8
- package/dist/components/connectivity/connection-gate.js.map +1 -1
- package/dist/components/connectivity/connection-state.d.ts +12 -0
- package/dist/components/connectivity/connection-state.d.ts.map +1 -0
- package/dist/components/connectivity/connection-state.js +23 -0
- package/dist/components/connectivity/connection-state.js.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.js +4 -0
- package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
- package/dist/components/graph/graph-controls.js +4 -1
- package/dist/components/graph/graph-controls.js.map +1 -1
- package/dist/components/graph/graph-maintenance.d.ts +8 -0
- package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
- package/dist/components/graph/graph-maintenance.js +115 -0
- package/dist/components/graph/graph-maintenance.js.map +1 -0
- package/dist/components/graph/graph-page.d.ts.map +1 -1
- package/dist/components/graph/graph-page.js +11 -306
- package/dist/components/graph/graph-page.js.map +1 -1
- package/dist/components/graph/graph-scene.d.ts +8 -0
- package/dist/components/graph/graph-scene.d.ts.map +1 -0
- package/dist/components/graph/graph-scene.js +208 -0
- package/dist/components/graph/graph-scene.js.map +1 -0
- package/dist/components/graph/graph-spinner.d.ts +2 -0
- package/dist/components/graph/graph-spinner.d.ts.map +1 -0
- package/dist/components/graph/graph-spinner.js +6 -0
- package/dist/components/graph/graph-spinner.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -1
- package/dist/components/layout/app-shell.js +2 -1
- package/dist/components/layout/app-shell.js.map +1 -1
- package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/add-modal.js +47 -4
- package/dist/components/quick-actions/add-modal.js.map +1 -1
- package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/search-modal.js +46 -18
- package/dist/components/quick-actions/search-modal.js.map +1 -1
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/streaming-modal.js +4 -1
- package/dist/components/quick-actions/streaming-modal.js.map +1 -1
- package/dist/components/settings/devices-agents-tab.d.ts +5 -0
- package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
- package/dist/components/settings/devices-agents-tab.js +96 -0
- package/dist/components/settings/devices-agents-tab.js.map +1 -0
- package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
- package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
- package/dist/components/settings/one-time-agent-credential.js +55 -0
- package/dist/components/settings/one-time-agent-credential.js.map +1 -0
- package/dist/components/settings/settings-panel.d.ts.map +1 -1
- package/dist/components/settings/settings-panel.js +16 -3
- package/dist/components/settings/settings-panel.js.map +1 -1
- package/dist/hooks/use-vpn-status.d.ts +6 -1
- package/dist/hooks/use-vpn-status.d.ts.map +1 -1
- package/dist/hooks/use-vpn-status.js +67 -30
- package/dist/hooks/use-vpn-status.js.map +1 -1
- package/dist/hooks/use-websocket.d.ts +3 -2
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +65 -254
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
- package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/activity.js +10 -0
- package/dist/hooks/websocket-handlers/activity.js.map +1 -0
- package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
- package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/chat.js +175 -0
- package/dist/hooks/websocket-handlers/chat.js.map +1 -0
- package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
- package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/connection.js +27 -0
- package/dist/hooks/websocket-handlers/connection.js.map +1 -0
- package/dist/hooks/websocket-handlers/index.d.ts +4 -0
- package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/index.js +19 -0
- package/dist/hooks/websocket-handlers/index.js.map +1 -0
- package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
- package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/mask.js +14 -0
- package/dist/hooks/websocket-handlers/mask.js.map +1 -0
- package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
- package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/provider.js +31 -0
- package/dist/hooks/websocket-handlers/provider.js.map +1 -0
- package/dist/hooks/websocket-handlers/types.d.ts +24 -0
- package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/types.js +2 -0
- package/dist/hooks/websocket-handlers/types.js.map +1 -0
- package/dist/lib/api-client.d.ts +32 -0
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +11 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/format-time.d.ts +3 -0
- package/dist/lib/format-time.d.ts.map +1 -0
- package/dist/lib/format-time.js +25 -0
- package/dist/lib/format-time.js.map +1 -0
- package/dist/lib/push-registration.d.ts +7 -0
- package/dist/lib/push-registration.d.ts.map +1 -0
- package/dist/lib/push-registration.js +26 -0
- package/dist/lib/push-registration.js.map +1 -0
- package/dist/stores/activity-store-helpers.d.ts +50 -0
- package/dist/stores/activity-store-helpers.d.ts.map +1 -0
- package/dist/stores/activity-store-helpers.js +143 -0
- package/dist/stores/activity-store-helpers.js.map +1 -0
- package/dist/stores/activity-store-types.d.ts +48 -0
- package/dist/stores/activity-store-types.d.ts.map +1 -0
- package/dist/stores/activity-store-types.js +2 -0
- package/dist/stores/activity-store-types.js.map +1 -0
- package/dist/stores/activity-store.d.ts +2 -78
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +22 -142
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/stores/connection-store.d.ts +14 -2
- package/dist/stores/connection-store.d.ts.map +1 -1
- package/dist/stores/connection-store.js +17 -0
- package/dist/stores/connection-store.js.map +1 -1
- package/dist/stores/file-store.d.ts.map +1 -1
- package/dist/stores/file-store.js +54 -18
- package/dist/stores/file-store.js.map +1 -1
- package/dist/stores/principal-store.d.ts +16 -0
- package/dist/stores/principal-store.d.ts.map +1 -0
- package/dist/stores/principal-store.js +29 -0
- package/dist/stores/principal-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +1 -1
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +6 -477
- package/src/components/activity/activity-rollups.tsx +94 -0
- package/src/components/activity/activity-run-detail.tsx +301 -0
- package/src/components/activity/activity-run-list.tsx +97 -0
- package/src/components/chat/brain-markdown-code.tsx +60 -0
- package/src/components/chat/brain-markdown-entities.tsx +116 -0
- package/src/components/chat/brain-markdown-links.tsx +247 -0
- package/src/components/chat/brain-markdown-share.ts +47 -0
- package/src/components/chat/brain-markdown.tsx +27 -535
- package/src/components/chat/composer.tsx +14 -1
- package/src/components/chat/session-drawer.tsx +29 -10
- package/src/components/chat/tool-views.tsx +2 -13
- package/src/components/chat/use-markdown-highlight.ts +63 -0
- package/src/components/connectivity/connection-gate.tsx +128 -14
- package/src/components/connectivity/connection-state.ts +37 -0
- package/src/components/graph/graph-canvas-lazy.tsx +4 -0
- package/src/components/graph/graph-controls.tsx +4 -1
- package/src/components/graph/graph-maintenance.tsx +304 -0
- package/src/components/graph/graph-page.tsx +11 -840
- package/src/components/graph/graph-scene.tsx +548 -0
- package/src/components/graph/graph-spinner.tsx +9 -0
- package/src/components/layout/app-shell.tsx +2 -0
- package/src/components/quick-actions/add-modal.tsx +49 -4
- package/src/components/quick-actions/search-modal.tsx +46 -18
- package/src/components/quick-actions/streaming-modal.tsx +3 -1
- package/src/components/settings/devices-agents-tab.tsx +231 -0
- package/src/components/settings/one-time-agent-credential.tsx +131 -0
- package/src/components/settings/settings-panel.tsx +20 -5
- package/src/hooks/use-vpn-status.ts +71 -31
- package/src/hooks/use-websocket.ts +74 -275
- package/src/hooks/websocket-handlers/activity.ts +13 -0
- package/src/hooks/websocket-handlers/chat.ts +210 -0
- package/src/hooks/websocket-handlers/connection.ts +30 -0
- package/src/hooks/websocket-handlers/index.ts +59 -0
- package/src/hooks/websocket-handlers/mask.ts +17 -0
- package/src/hooks/websocket-handlers/provider.ts +35 -0
- package/src/hooks/websocket-handlers/types.ts +27 -0
- package/src/lib/api-client.ts +40 -1
- package/src/lib/format-time.ts +20 -0
- package/src/lib/push-registration.ts +29 -0
- package/src/stores/activity-store-helpers.ts +164 -0
- package/src/stores/activity-store-types.ts +55 -0
- package/src/stores/activity-store.ts +21 -217
- package/src/stores/connection-store.ts +33 -2
- package/src/stores/file-store.ts +42 -12
- package/src/stores/principal-store.ts +38 -0
- package/src/stores/ui-store.ts +1 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { Suspense, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ChevronDown,
|
|
4
|
+
ChevronUp,
|
|
5
|
+
List,
|
|
6
|
+
Loader2,
|
|
7
|
+
TriangleAlert,
|
|
8
|
+
X,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import type {
|
|
11
|
+
GraphMaintenanceResponse,
|
|
12
|
+
GraphNodePayload,
|
|
13
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
14
|
+
|
|
15
|
+
import { useGraphStore } from "../../stores/graph-store.js";
|
|
16
|
+
import { useUIStore } from "../../stores/ui-store.js";
|
|
17
|
+
import { useFileStore } from "../../stores/file-store.js";
|
|
18
|
+
import { apiBase } from "../../lib/backend.js";
|
|
19
|
+
import { GraphControls } from "./graph-controls.js";
|
|
20
|
+
import { NodePopover } from "./node-popover.js";
|
|
21
|
+
import { GraphEmptyState, Mono } from "./graph-empty-state.js";
|
|
22
|
+
import { GraphCanvas } from "./graph-canvas-lazy.js";
|
|
23
|
+
import { CenteredSpinner } from "./graph-spinner.js";
|
|
24
|
+
import {
|
|
25
|
+
OTHER_COLOR,
|
|
26
|
+
assignFolderColors,
|
|
27
|
+
buildQuery,
|
|
28
|
+
communityColor,
|
|
29
|
+
distanceColor,
|
|
30
|
+
groupCommunities,
|
|
31
|
+
matchScene,
|
|
32
|
+
topLevelDir,
|
|
33
|
+
} from "./lib/graph-helpers.js";
|
|
34
|
+
import { useGraphTheme } from "./use-graph-theme.js";
|
|
35
|
+
import { cn } from "../../lib/utils.js";
|
|
36
|
+
|
|
37
|
+
/** Client force layout is a fallback for repos whose FA2 layout was skipped —
|
|
38
|
+
* beyond this it would freeze the tab, so we refuse and say why. */
|
|
39
|
+
const CLIENT_LAYOUT_NODE_CAP = 2000;
|
|
40
|
+
|
|
41
|
+
/** The canvas plus everything overlaid on it. Only mounts with data present. */
|
|
42
|
+
export function SceneBody() {
|
|
43
|
+
const mode = useGraphStore((s) => s.mode);
|
|
44
|
+
const meta = useGraphStore((s) => s.meta);
|
|
45
|
+
const subgraph = useGraphStore((s) => s.subgraph)!;
|
|
46
|
+
const selectedId = useGraphStore((s) => s.selectedId);
|
|
47
|
+
const hoveredId = useGraphStore((s) => s.hoveredId);
|
|
48
|
+
const select = useGraphStore((s) => s.select);
|
|
49
|
+
const hover = useGraphStore((s) => s.hover);
|
|
50
|
+
const sceneQuery = useGraphStore((s) => s.sceneQuery);
|
|
51
|
+
const clustersSizeBy = useGraphStore((s) => s.clustersSizeBy);
|
|
52
|
+
const discoveryColorBy = useGraphStore((s) => s.discoveryColorBy);
|
|
53
|
+
const theme = useGraphTheme();
|
|
54
|
+
const [listOpen, setListOpen] = useState(false);
|
|
55
|
+
|
|
56
|
+
const matchIds = useMemo(
|
|
57
|
+
() => matchScene(subgraph.nodes, sceneQuery),
|
|
58
|
+
[subgraph.nodes, sceneQuery]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const layout =
|
|
62
|
+
mode === "clusters"
|
|
63
|
+
? subgraph.nodes.some((n) => n.x !== undefined)
|
|
64
|
+
? ("fixed" as const)
|
|
65
|
+
: ("force" as const)
|
|
66
|
+
: mode === "discovery"
|
|
67
|
+
? ("radial" as const)
|
|
68
|
+
: ("force" as const);
|
|
69
|
+
|
|
70
|
+
const maxDistance = useMemo(
|
|
71
|
+
() => Math.max(1, ...subgraph.nodes.map((n) => n.distance ?? 0)),
|
|
72
|
+
[subgraph.nodes]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const folderColors = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
mode === "discovery" && discoveryColorBy === "folder"
|
|
78
|
+
? assignFolderColors(subgraph.nodes.map((n) => n.path))
|
|
79
|
+
: null,
|
|
80
|
+
[mode, discoveryColorBy, subgraph.nodes]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const nodeColor = useMemo(() => {
|
|
84
|
+
if (mode === "discovery") {
|
|
85
|
+
if (folderColors) {
|
|
86
|
+
return (node: GraphNodePayload) =>
|
|
87
|
+
node.virtual
|
|
88
|
+
? theme.nodeSelected
|
|
89
|
+
: (folderColors.get(topLevelDir(node.path)) ?? OTHER_COLOR);
|
|
90
|
+
}
|
|
91
|
+
return (node: GraphNodePayload) =>
|
|
92
|
+
node.virtual ? theme.nodeSelected : distanceColor(node.distance ?? maxDistance);
|
|
93
|
+
}
|
|
94
|
+
return (node: GraphNodePayload) =>
|
|
95
|
+
node.community !== undefined ? communityColor(node.community) : theme.node;
|
|
96
|
+
}, [mode, folderColors, maxDistance, theme]);
|
|
97
|
+
|
|
98
|
+
const centerId = useMemo(
|
|
99
|
+
() =>
|
|
100
|
+
mode === "local"
|
|
101
|
+
? (subgraph.nodes.find((n) => n.distance === 0)?.id ?? null)
|
|
102
|
+
: null,
|
|
103
|
+
[mode, subgraph.nodes]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const edgeColor = useMemo(() => {
|
|
107
|
+
if (mode !== "local" || centerId === null) return undefined;
|
|
108
|
+
// Backlinks into the center get the accent so in/out reads at a glance.
|
|
109
|
+
return (edge: { source: number; target: number }) =>
|
|
110
|
+
edge.target === centerId ? theme.edgeHighlight : undefined;
|
|
111
|
+
}, [mode, centerId, theme]);
|
|
112
|
+
|
|
113
|
+
const selectedNode = useMemo(
|
|
114
|
+
() =>
|
|
115
|
+
selectedId === null
|
|
116
|
+
? null
|
|
117
|
+
: (subgraph.nodes.find((n) => n.id === selectedId) ?? null),
|
|
118
|
+
[selectedId, subgraph.nodes]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const communityLabel =
|
|
122
|
+
selectedNode?.community !== undefined
|
|
123
|
+
? (meta?.communities.find((c) => c.community === selectedNode.community)
|
|
124
|
+
?.label ?? null)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
// Clusters whose FA2 layout was skipped at index time: client force layout
|
|
128
|
+
// is a bounded courtesy. Over the cap, only the canvas is replaced — the
|
|
129
|
+
// legend and controls stay live so filtering to one topic (a subset under
|
|
130
|
+
// the cap) still works.
|
|
131
|
+
const layoutBlocked =
|
|
132
|
+
mode === "clusters" &&
|
|
133
|
+
layout === "force" &&
|
|
134
|
+
subgraph.nodes.length > CLIENT_LAYOUT_NODE_CAP;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
{layoutBlocked ? (
|
|
139
|
+
<GraphEmptyState icon="warn" title="Too large to lay out here">
|
|
140
|
+
The cluster layout was skipped at index time (corpus over the layout
|
|
141
|
+
cap), and {subgraph.nodes.length.toLocaleString()} notes are too many
|
|
142
|
+
to arrange in the browser. Pick a topic from the legend to view a
|
|
143
|
+
subset.
|
|
144
|
+
</GraphEmptyState>
|
|
145
|
+
) : (
|
|
146
|
+
<Suspense fallback={<CenteredSpinner />}>
|
|
147
|
+
<GraphCanvas
|
|
148
|
+
data={subgraph}
|
|
149
|
+
layout={layout}
|
|
150
|
+
selectedId={selectedId}
|
|
151
|
+
hoveredId={hoveredId}
|
|
152
|
+
matchIds={matchIds}
|
|
153
|
+
sizeBy={mode === "clusters" ? clustersSizeBy : "degree"}
|
|
154
|
+
rings={mode === "discovery" ? maxDistance : 0}
|
|
155
|
+
nodeColor={nodeColor}
|
|
156
|
+
edgeColor={edgeColor}
|
|
157
|
+
onSelect={select}
|
|
158
|
+
onHover={hover}
|
|
159
|
+
/>
|
|
160
|
+
</Suspense>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<GraphControls />
|
|
164
|
+
|
|
165
|
+
{mode === "clusters" && <ClusterLegend />}
|
|
166
|
+
{mode === "discovery" && folderColors && (
|
|
167
|
+
<FolderLegend colors={folderColors} />
|
|
168
|
+
)}
|
|
169
|
+
{mode === "discovery" && (
|
|
170
|
+
<UnreachableTray count={subgraph.unreachableCount} />
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Accessible / mobile-friendly node list — the canvas itself is aria-hidden */}
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => setListOpen((v) => !v)}
|
|
176
|
+
className="absolute bottom-3 right-3 z-10 flex h-11 w-11 items-center justify-center rounded-full border border-border bg-surface-overlay text-foreground shadow-lg"
|
|
177
|
+
title="Node list"
|
|
178
|
+
aria-expanded={listOpen}
|
|
179
|
+
>
|
|
180
|
+
<List className="h-4.5 w-4.5" />
|
|
181
|
+
</button>
|
|
182
|
+
{listOpen && <NodeList onClose={() => setListOpen(false)} />}
|
|
183
|
+
|
|
184
|
+
{selectedNode && (
|
|
185
|
+
<NodePopover node={selectedNode} communityLabel={communityLabel} />
|
|
186
|
+
)}
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Clusters mode: interactive community legend, top-left. */
|
|
192
|
+
function ClusterLegend() {
|
|
193
|
+
const meta = useGraphStore((s) => s.meta);
|
|
194
|
+
const clusters = useGraphStore((s) => s.clusters);
|
|
195
|
+
const setClustersParams = useGraphStore((s) => s.setClustersParams);
|
|
196
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
197
|
+
|
|
198
|
+
const groups = useMemo(
|
|
199
|
+
() => groupCommunities(meta?.communities ?? []),
|
|
200
|
+
[meta?.communities]
|
|
201
|
+
);
|
|
202
|
+
if (groups.major.length === 0) return null;
|
|
203
|
+
|
|
204
|
+
const shown = groups.major.slice(0, 8);
|
|
205
|
+
const foldedCount =
|
|
206
|
+
groups.major.length - shown.length + groups.singletonCount;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="absolute left-3 top-3 z-10 w-56 max-w-[calc(100vw-5rem)] rounded-xl border border-border bg-surface-overlay/95 shadow-xl backdrop-blur">
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => setCollapsed((v) => !v)}
|
|
212
|
+
className="flex w-full items-center justify-between px-3 py-2 text-xs font-medium text-foreground"
|
|
213
|
+
aria-expanded={!collapsed}
|
|
214
|
+
>
|
|
215
|
+
Topics
|
|
216
|
+
{collapsed ? (
|
|
217
|
+
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
218
|
+
) : (
|
|
219
|
+
<ChevronUp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
220
|
+
)}
|
|
221
|
+
</button>
|
|
222
|
+
{!collapsed && (
|
|
223
|
+
<div className="max-h-64 overflow-y-auto px-1 pb-2">
|
|
224
|
+
{shown.map((c) => {
|
|
225
|
+
const active = clusters.community === c.community;
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
key={c.community}
|
|
229
|
+
onClick={() =>
|
|
230
|
+
setClustersParams({ community: active ? null : c.community })
|
|
231
|
+
}
|
|
232
|
+
className={cn(
|
|
233
|
+
"flex min-h-9 w-full items-center gap-2 rounded-lg px-2 py-1 text-left transition-colors",
|
|
234
|
+
active ? "bg-surface-raised" : "hover:bg-surface-raised/60"
|
|
235
|
+
)}
|
|
236
|
+
title={
|
|
237
|
+
active
|
|
238
|
+
? "Show all topics"
|
|
239
|
+
: `Show only this topic (${c.size} notes)`
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
<span
|
|
243
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
244
|
+
style={{ backgroundColor: communityColor(c.community) }}
|
|
245
|
+
/>
|
|
246
|
+
<span className="min-w-0 flex-1 truncate text-xs text-foreground">
|
|
247
|
+
{c.label ?? `Topic ${c.community + 1}`}
|
|
248
|
+
</span>
|
|
249
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
250
|
+
{c.size}
|
|
251
|
+
</span>
|
|
252
|
+
</button>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
{foldedCount > 0 && (
|
|
256
|
+
<div className="flex items-center gap-2 px-2 py-1.5">
|
|
257
|
+
<span
|
|
258
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
259
|
+
style={{ backgroundColor: OTHER_COLOR }}
|
|
260
|
+
/>
|
|
261
|
+
<span className="text-[11px] text-muted-foreground">
|
|
262
|
+
{foldedCount} smaller topic{foldedCount === 1 ? "" : "s"}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
{clusters.community !== null && (
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => setClustersParams({ community: null })}
|
|
269
|
+
className="mx-2 mt-1 flex min-h-8 items-center gap-1 rounded-lg bg-surface-raised px-2 py-1 text-[11px] text-foreground hover:bg-surface"
|
|
270
|
+
>
|
|
271
|
+
<X className="h-3 w-3" />
|
|
272
|
+
Clear topic filter
|
|
273
|
+
</button>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Discovery + color-by-folder: which top-level dir wears which color. */
|
|
282
|
+
function FolderLegend({ colors }: { colors: Map<string, string> }) {
|
|
283
|
+
const entries = [...colors.entries()].filter(([, c]) => c !== OTHER_COLOR);
|
|
284
|
+
const otherCount = colors.size - entries.length;
|
|
285
|
+
if (entries.length === 0) return null;
|
|
286
|
+
return (
|
|
287
|
+
<div className="absolute left-3 top-3 z-10 w-48 rounded-xl border border-border bg-surface-overlay/95 px-3 py-2 shadow-xl backdrop-blur">
|
|
288
|
+
<div className="mb-1 text-xs font-medium text-foreground">Folders</div>
|
|
289
|
+
{entries.map(([dir, color]) => (
|
|
290
|
+
<div key={dir} className="flex min-h-6 items-center gap-2">
|
|
291
|
+
<span
|
|
292
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
293
|
+
style={{ backgroundColor: color }}
|
|
294
|
+
/>
|
|
295
|
+
<span className="truncate font-mono text-[11px] text-muted-foreground">
|
|
296
|
+
{dir || "(root)"}
|
|
297
|
+
</span>
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
{otherCount > 0 && (
|
|
301
|
+
<div className="flex min-h-6 items-center gap-2">
|
|
302
|
+
<span
|
|
303
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
304
|
+
style={{ backgroundColor: OTHER_COLOR }}
|
|
305
|
+
/>
|
|
306
|
+
<span className="text-[11px] text-muted-foreground">
|
|
307
|
+
{otherCount} more
|
|
308
|
+
</span>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Discovery: how much of the corpus this root does NOT reach. The count comes
|
|
317
|
+
* from the server (`unreachableCount`) — a scene-size subtraction would lie
|
|
318
|
+
* under depth limits, truncation, and the virtual root. An older server
|
|
319
|
+
* doesn't send it; then the tray only appears for the default root, where the
|
|
320
|
+
* maintenance findings still provide the exact list, and shows no number.
|
|
321
|
+
*/
|
|
322
|
+
function UnreachableTray({ count }: { count: number | undefined }) {
|
|
323
|
+
const discovery = useGraphStore((s) => s.discovery);
|
|
324
|
+
const staleDays = useGraphStore((s) => s.maintenance.staleDays);
|
|
325
|
+
const openFile = useFileStore((s) => s.openFile);
|
|
326
|
+
const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
|
|
327
|
+
const [open, setOpen] = useState(false);
|
|
328
|
+
const [nodes, setNodes] = useState<GraphNodePayload[] | null>(null);
|
|
329
|
+
|
|
330
|
+
const isDefaultRoot = !discovery.root;
|
|
331
|
+
// Older server, no count field: the maintenance list is the only honest
|
|
332
|
+
// source, so it must be fetched EAGERLY — the chip may only appear once
|
|
333
|
+
// the list proves there is something to warn about.
|
|
334
|
+
const needsFallbackProof = count === undefined && isDefaultRoot;
|
|
335
|
+
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
const wanted = (open || needsFallbackProof) && isDefaultRoot;
|
|
338
|
+
if (!wanted || nodes !== null) return;
|
|
339
|
+
let cancelled = false;
|
|
340
|
+
void (async () => {
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(
|
|
343
|
+
`${apiBase()}/graph/maintenance?${buildQuery({ staleDays })}`
|
|
344
|
+
);
|
|
345
|
+
if (!res.ok) return;
|
|
346
|
+
const data = (await res.json()) as GraphMaintenanceResponse;
|
|
347
|
+
if (!cancelled) setNodes(data.unreachable);
|
|
348
|
+
} catch {
|
|
349
|
+
// The tray degrades to count-only (or stays hidden in fallback mode).
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
return () => {
|
|
353
|
+
cancelled = true;
|
|
354
|
+
};
|
|
355
|
+
}, [open, needsFallbackProof, isDefaultRoot, nodes, staleDays]);
|
|
356
|
+
|
|
357
|
+
if (count === 0) return null;
|
|
358
|
+
// No server count and no exact list available: nothing honest to show.
|
|
359
|
+
if (count === undefined && !isDefaultRoot) return null;
|
|
360
|
+
// Fallback mode: silent until the maintenance list confirms unreachable
|
|
361
|
+
// notes exist — an unproven warning chip would cry wolf on clean repos.
|
|
362
|
+
if (needsFallbackProof && (nodes === null || nodes.length === 0)) return null;
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<div className="absolute bottom-3 left-3 z-10 max-w-[calc(100vw-5rem)]">
|
|
366
|
+
{open && (
|
|
367
|
+
<div className="mb-2 flex max-h-56 w-64 flex-col overflow-hidden rounded-xl border border-border bg-surface-overlay/95 shadow-2xl backdrop-blur">
|
|
368
|
+
<div className="border-b border-border px-3 py-2 text-[11px] text-muted-foreground">
|
|
369
|
+
{isDefaultRoot
|
|
370
|
+
? "Not reachable from the root:"
|
|
371
|
+
: "Count only — pick the default root for the full list."}
|
|
372
|
+
</div>
|
|
373
|
+
{isDefaultRoot && (
|
|
374
|
+
<div className="flex-1 overflow-y-auto">
|
|
375
|
+
{(nodes ?? []).map((node) => (
|
|
376
|
+
<button
|
|
377
|
+
key={node.id}
|
|
378
|
+
onClick={() => {
|
|
379
|
+
setFilePanelOpen(true);
|
|
380
|
+
void openFile(node.path);
|
|
381
|
+
}}
|
|
382
|
+
className="flex min-h-9 w-full flex-col justify-center px-3 py-1 text-left hover:bg-surface-raised/60"
|
|
383
|
+
>
|
|
384
|
+
<span className="truncate text-xs text-foreground">
|
|
385
|
+
{node.title || node.path}
|
|
386
|
+
</span>
|
|
387
|
+
</button>
|
|
388
|
+
))}
|
|
389
|
+
{nodes === null && (
|
|
390
|
+
<div className="flex justify-center py-3">
|
|
391
|
+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => setOpen((v) => !v)}
|
|
400
|
+
aria-expanded={open}
|
|
401
|
+
className="flex min-h-9 items-center gap-1.5 rounded-full border border-border bg-surface-overlay px-3 py-1.5 text-[11px] text-muted-foreground shadow-lg transition-colors hover:text-foreground"
|
|
402
|
+
>
|
|
403
|
+
<TriangleAlert className="h-3 w-3" />
|
|
404
|
+
{count !== undefined
|
|
405
|
+
? `${count} unreachable`
|
|
406
|
+
: `${nodes!.length} unreachable`}
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Discovery with no root at all (no entry file in the repo): make picking one
|
|
414
|
+
* inviting — index notes are the natural candidates.
|
|
415
|
+
*/
|
|
416
|
+
export function DiscoveryStart() {
|
|
417
|
+
const setDiscoveryParams = useGraphStore((s) => s.setDiscoveryParams);
|
|
418
|
+
const [candidates, setCandidates] = useState<
|
|
419
|
+
{ path: string; title: string }[] | null
|
|
420
|
+
>(null);
|
|
421
|
+
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
let cancelled = false;
|
|
424
|
+
void (async () => {
|
|
425
|
+
try {
|
|
426
|
+
const res = await fetch(
|
|
427
|
+
`${apiBase()}/brain/list?${buildQuery({ type: "index", limit: 6 })}`
|
|
428
|
+
);
|
|
429
|
+
if (!res.ok) return;
|
|
430
|
+
const data = (await res.json()) as {
|
|
431
|
+
results?: { path: string; title?: string }[];
|
|
432
|
+
};
|
|
433
|
+
if (!cancelled)
|
|
434
|
+
setCandidates(
|
|
435
|
+
(data.results ?? []).map((r) => ({
|
|
436
|
+
path: r.path,
|
|
437
|
+
title: r.title ?? r.path,
|
|
438
|
+
}))
|
|
439
|
+
);
|
|
440
|
+
} catch {
|
|
441
|
+
if (!cancelled) setCandidates([]);
|
|
442
|
+
}
|
|
443
|
+
})();
|
|
444
|
+
return () => {
|
|
445
|
+
cancelled = true;
|
|
446
|
+
};
|
|
447
|
+
}, []);
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<>
|
|
451
|
+
<div className="flex h-full items-center justify-center p-6">
|
|
452
|
+
<div className="flex w-full max-w-md flex-col gap-3 rounded-xl border border-border bg-surface px-8 py-10 text-center">
|
|
453
|
+
<h2 className="font-display text-lg text-foreground">
|
|
454
|
+
Where should discovery start?
|
|
455
|
+
</h2>
|
|
456
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
457
|
+
Discovery lays your notes out by how many links they are away from
|
|
458
|
+
a starting point. This repo has no <Mono>AGENTS.md</Mono> entry
|
|
459
|
+
file, so pick one — index notes work well:
|
|
460
|
+
</p>
|
|
461
|
+
{candidates === null ? (
|
|
462
|
+
<div className="flex justify-center py-2">
|
|
463
|
+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
|
464
|
+
</div>
|
|
465
|
+
) : (
|
|
466
|
+
<div className="flex flex-col gap-1.5">
|
|
467
|
+
{candidates.map((c) => (
|
|
468
|
+
<button
|
|
469
|
+
key={c.path}
|
|
470
|
+
onClick={() => setDiscoveryParams({ root: c.path })}
|
|
471
|
+
className="flex min-h-10 flex-col justify-center rounded-lg border border-border bg-surface-raised px-3 py-1.5 text-left transition-colors hover:border-primary/40"
|
|
472
|
+
>
|
|
473
|
+
<span className="truncate text-xs text-foreground">
|
|
474
|
+
{c.title}
|
|
475
|
+
</span>
|
|
476
|
+
<span className="truncate font-mono text-[10px] text-muted-foreground/60">
|
|
477
|
+
{c.path}
|
|
478
|
+
</span>
|
|
479
|
+
</button>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
<p className="text-[11px] text-muted-foreground">
|
|
484
|
+
…or search any note in the options panel.
|
|
485
|
+
</p>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
<GraphControls />
|
|
489
|
+
</>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Focusable listing of the current scene, sorted by connectedness. */
|
|
494
|
+
function NodeList({ onClose }: { onClose: () => void }) {
|
|
495
|
+
const subgraph = useGraphStore((s) => s.subgraph)!;
|
|
496
|
+
const select = useGraphStore((s) => s.select);
|
|
497
|
+
const selectedId = useGraphStore((s) => s.selectedId);
|
|
498
|
+
|
|
499
|
+
const sorted = useMemo(
|
|
500
|
+
() =>
|
|
501
|
+
[...subgraph.nodes].sort(
|
|
502
|
+
(a, b) => b.inDegree + b.outDegree - (a.inDegree + a.outDegree)
|
|
503
|
+
),
|
|
504
|
+
[subgraph.nodes]
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<nav
|
|
509
|
+
aria-label="Graph nodes"
|
|
510
|
+
className="absolute bottom-16 right-3 z-10 flex max-h-[55dvh] w-72 max-w-[calc(100vw-1.5rem)] flex-col overflow-hidden rounded-xl border border-border bg-surface-overlay/95 shadow-2xl backdrop-blur"
|
|
511
|
+
>
|
|
512
|
+
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
|
513
|
+
<span className="text-xs font-medium text-foreground">
|
|
514
|
+
{sorted.length} note{sorted.length === 1 ? "" : "s"}
|
|
515
|
+
</span>
|
|
516
|
+
<button
|
|
517
|
+
onClick={onClose}
|
|
518
|
+
className="rounded p-1 text-muted-foreground hover:text-foreground"
|
|
519
|
+
title="Close list"
|
|
520
|
+
>
|
|
521
|
+
<X className="h-3.5 w-3.5" />
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
<div className="flex-1 overflow-y-auto">
|
|
525
|
+
{sorted.map((node) => (
|
|
526
|
+
<button
|
|
527
|
+
key={node.id}
|
|
528
|
+
onClick={() => select(node.id)}
|
|
529
|
+
className={cn(
|
|
530
|
+
"flex min-h-11 w-full flex-col justify-center px-3 py-1.5 text-left transition-colors",
|
|
531
|
+
node.id === selectedId
|
|
532
|
+
? "bg-surface-raised"
|
|
533
|
+
: "hover:bg-surface-raised/60"
|
|
534
|
+
)}
|
|
535
|
+
>
|
|
536
|
+
<span className="truncate text-xs text-foreground">
|
|
537
|
+
{node.title || node.path}
|
|
538
|
+
</span>
|
|
539
|
+
<span className="truncate font-mono text-[10px] text-muted-foreground/60">
|
|
540
|
+
{node.inDegree + node.outDegree} link
|
|
541
|
+
{node.inDegree + node.outDegree === 1 ? "" : "s"} · {node.path}
|
|
542
|
+
</span>
|
|
543
|
+
</button>
|
|
544
|
+
))}
|
|
545
|
+
</div>
|
|
546
|
+
</nav>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
@@ -3,6 +3,7 @@ import { SideRail } from "./side-rail.js";
|
|
|
3
3
|
import { MobileTabBar } from "./mobile-tab-bar.js";
|
|
4
4
|
import { useChatStore, activeChat } from "../../stores/chat-store.js";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { OneTimeAgentCredentialDialog } from "../settings/one-time-agent-credential.js";
|
|
6
7
|
|
|
7
8
|
export function AppShell({ children }: { children: ReactNode }) {
|
|
8
9
|
const isStreaming = useChatStore((s) => activeChat(s).isStreaming);
|
|
@@ -20,6 +21,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|
|
20
21
|
</main>
|
|
21
22
|
</div>
|
|
22
23
|
<MobileTabBar />
|
|
24
|
+
<OneTimeAgentCredentialDialog />
|
|
23
25
|
</div>
|
|
24
26
|
);
|
|
25
27
|
}
|
|
@@ -27,6 +27,10 @@ export function AddPanel({
|
|
|
27
27
|
const [tags, setTags] = useState("");
|
|
28
28
|
const [state, setState] = useState<State>("editing");
|
|
29
29
|
const [error, setError] = useState("");
|
|
30
|
+
const [savedPath, setSavedPath] = useState("");
|
|
31
|
+
const [indexed, setIndexed] = useState<boolean | undefined>();
|
|
32
|
+
const [indexing, setIndexing] = useState(false);
|
|
33
|
+
const operation = useRef({ epoch: 0 }).current;
|
|
30
34
|
// Types already in the brain, offered as completions — `brain add` accepts
|
|
31
35
|
// any string, so this is a hint list, not a closed set.
|
|
32
36
|
const [knownTypes, setKnownTypes] = useState<string[]>([]);
|
|
@@ -35,6 +39,10 @@ export function AddPanel({
|
|
|
35
39
|
|
|
36
40
|
useEffect(() => {
|
|
37
41
|
if (!open) return;
|
|
42
|
+
operation.epoch++;
|
|
43
|
+
setSavedPath("");
|
|
44
|
+
setIndexed(undefined);
|
|
45
|
+
setIndexing(false);
|
|
38
46
|
setContent("");
|
|
39
47
|
setTitle("");
|
|
40
48
|
setType("");
|
|
@@ -55,31 +63,57 @@ export function AddPanel({
|
|
|
55
63
|
});
|
|
56
64
|
|
|
57
65
|
return () => {
|
|
66
|
+
operation.epoch++;
|
|
58
67
|
cancelled = true;
|
|
59
68
|
clearTimeout(t);
|
|
60
69
|
};
|
|
61
|
-
}, [open]);
|
|
70
|
+
}, [open, operation]);
|
|
62
71
|
|
|
63
72
|
async function handleSave() {
|
|
64
73
|
const body = content.trim();
|
|
65
|
-
if (!body || state
|
|
74
|
+
if (!body || (state !== "editing" && state !== "error")) return;
|
|
75
|
+
const current = ++operation.epoch;
|
|
66
76
|
|
|
67
77
|
setState("saving");
|
|
68
78
|
setError("");
|
|
69
79
|
try {
|
|
70
|
-
await api.brainAdd(body, {
|
|
80
|
+
const result = await api.brainAdd(body, {
|
|
71
81
|
type: type.trim() || undefined,
|
|
72
82
|
title: title.trim() || undefined,
|
|
73
83
|
tags: parseTags(tags).length ? parseTags(tags) : undefined,
|
|
74
84
|
});
|
|
85
|
+
if (current !== operation.epoch) return;
|
|
86
|
+
setSavedPath(result.path ?? "");
|
|
87
|
+
setIndexed(result.indexed);
|
|
88
|
+
setError(result.indexed === false ? result.indexError || "Indexing failed." : "");
|
|
75
89
|
setState("saved");
|
|
76
90
|
} catch (err: any) {
|
|
91
|
+
if (current !== operation.epoch) return;
|
|
77
92
|
setError(err?.message || "Add failed");
|
|
78
93
|
setState("error");
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
async function retryIndex() {
|
|
98
|
+
if (indexing) return;
|
|
99
|
+
const current = ++operation.epoch;
|
|
100
|
+
setIndexing(true);
|
|
101
|
+
setError("");
|
|
102
|
+
try {
|
|
103
|
+
await api.brainIndex();
|
|
104
|
+
if (current === operation.epoch) setIndexed(true);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (current === operation.epoch) setError(err instanceof Error ? err.message : "Indexing failed.");
|
|
107
|
+
} finally {
|
|
108
|
+
if (current === operation.epoch) setIndexing(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
function handleAddAnother() {
|
|
113
|
+
operation.epoch++;
|
|
114
|
+
setSavedPath("");
|
|
115
|
+
setIndexed(undefined);
|
|
116
|
+
setIndexing(false);
|
|
83
117
|
setContent("");
|
|
84
118
|
setTitle("");
|
|
85
119
|
setType("");
|
|
@@ -107,8 +141,19 @@ export function AddPanel({
|
|
|
107
141
|
<CheckCircle2 className="h-8 w-8 text-primary" />
|
|
108
142
|
<p className="text-sm text-foreground">Added to your brain.</p>
|
|
109
143
|
<p className="text-xs text-muted-foreground">
|
|
110
|
-
|
|
144
|
+
{indexed === false ? "Saved, but not indexed. Your content is safe; search may not find it yet."
|
|
145
|
+
: indexed === true ? "Saved and indexed. Run a sync when you want to push it."
|
|
146
|
+
: "Saved. This server did not report whether indexing completed."}
|
|
111
147
|
</p>
|
|
148
|
+
{savedPath && <p className="break-all text-xs text-muted-foreground">{savedPath}</p>}
|
|
149
|
+
{indexed === false && (
|
|
150
|
+
<div className="space-y-2" role="status">
|
|
151
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
152
|
+
<button onClick={retryIndex} disabled={indexing} className="rounded-lg bg-primary px-4 py-1.5 text-xs text-primary-foreground disabled:opacity-40">
|
|
153
|
+
{indexing ? "Indexing..." : "Retry indexing"}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
112
157
|
<div className="flex gap-2">
|
|
113
158
|
<button
|
|
114
159
|
onClick={handleAddAnother}
|