@immediately-run/grove 0.1.2 → 0.1.3
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 +43 -115
- package/llms.txt +5 -2
- package/package.json +10 -8
- package/src/App.tsx +11 -7
- package/src/GroveApp.css +484 -152
- package/src/GroveWiki.tsx +105 -30
- package/src/components/AssetImage.tsx +1 -12
- package/src/components/Backlinks.tsx +2 -2
- package/src/components/Catalogue.test.tsx +157 -0
- package/src/components/ContentTheme.test.tsx +146 -0
- package/src/components/ContentTheme.tsx +111 -0
- package/src/components/DocList.infinite.test.tsx +177 -0
- package/src/components/DocList.tsx +82 -9
- package/src/components/Drawer.tsx +14 -2
- package/src/components/EntryHeader.tsx +23 -12
- package/src/components/EntryImage.test.tsx +210 -0
- package/src/components/EntryImage.tsx +47 -0
- package/src/components/Galleries.test.tsx +98 -0
- package/src/components/GroveAgent.test.tsx +330 -0
- package/src/components/GroveAgent.tsx +269 -150
- package/src/components/GroveNav.test.tsx +145 -5
- package/src/components/GroveNav.tsx +51 -8
- package/src/components/Icon.tsx +0 -1
- package/src/components/InlineProse.tsx +37 -0
- package/src/components/LayoutGallery.tsx +65 -0
- package/src/components/PageView.tsx +18 -3
- package/src/components/Search.test.tsx +113 -0
- package/src/components/Search.tsx +53 -18
- package/src/components/Sidebar.test.tsx +135 -0
- package/src/components/Sidebar.tsx +114 -12
- package/src/components/TableOfContents.test.tsx +27 -16
- package/src/components/ThemeAssets.test.tsx +109 -0
- package/src/components/ThemeAssets.tsx +63 -0
- package/src/components/ThemeGallery.tsx +31 -0
- package/src/components/Timeline.tsx +5 -2
- package/src/components/WikiLink.tsx +2 -2
- package/src/data/catalogue.ts +54 -0
- package/src/data/themeFonts.ts +58 -0
- package/src/data/themes.ts +16 -7
- package/src/hooks/{useCorpusMetadata.ts → useBundleMetadata.ts} +6 -6
- package/src/hooks/useContentComponents.ts +1 -1
- package/src/hooks/useEditAffordance.ts +17 -4
- package/src/hooks/useOverlayFocusDismiss.test.tsx +139 -0
- package/src/hooks/useOverlayFocusDismiss.ts +118 -0
- package/src/hooks/useScrollReset.test.tsx +132 -0
- package/src/hooks/useScrollReset.ts +52 -0
- package/src/index.css +9 -2
- package/src/lib/agentPrompt.test.ts +96 -0
- package/src/lib/agentPrompt.ts +86 -0
- package/src/lib/agentTools.test.ts +115 -0
- package/src/lib/agentTools.ts +132 -0
- package/src/lib/agentTranscript.ts +50 -0
- package/src/lib/assetPath.test.ts +36 -0
- package/src/lib/assetPath.ts +42 -0
- package/src/lib/collectionCalls.test.ts +69 -0
- package/src/lib/content.test.ts +10 -2
- package/src/lib/content.ts +7 -0
- package/src/lib/contentStylesheet.test.ts +80 -0
- package/src/lib/contentStylesheet.ts +103 -0
- package/src/lib/corpusScan.test.ts +37 -3
- package/src/lib/corpusScan.ts +17 -2
- package/src/lib/inlineProse.parity.test.ts +52 -0
- package/src/lib/layout.ts +29 -0
- package/src/lib/pageVariants.test.tsx +87 -0
- package/src/lib/queries.test.ts +33 -1
- package/src/lib/queries.ts +33 -1
- package/src/lib/reachCard.test.ts +94 -0
- package/src/lib/reachCard.ts +112 -0
- package/src/lib/shell.ts +7 -0
- package/src/lib/starterSweep.test.tsx +160 -0
- package/src/lib/starterSweep.ts +97 -0
- package/src/lib/themeAssets.test.ts +135 -0
- package/src/lib/themeAssets.ts +143 -0
- package/src/lib/themeSelection.test.ts +2 -2
- package/src/mdxComponents.ts +4 -0
- package/viewer-manifest.schema.json +37 -0
- package/viewer.manifest.json +132 -6
|
@@ -1,130 +1,243 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
runAgent,
|
|
4
|
+
createChatModelClient,
|
|
5
|
+
useChatProviderState,
|
|
6
|
+
useCatalog,
|
|
7
|
+
useAllMetadata,
|
|
8
|
+
useAgentContext,
|
|
9
|
+
renderAgentContext,
|
|
10
|
+
type AgentMessage,
|
|
11
|
+
} from '@immediately-run/sdk';
|
|
12
|
+
import { useShell, EDIT_REFUSED_NOTICE } from '../lib/shell';
|
|
13
|
+
import { useHeadings, useActiveHeading } from '../hooks/useHeadings';
|
|
14
|
+
import { useOverlayFocusDismiss } from '../hooks/useOverlayFocusDismiss';
|
|
15
|
+
import { getContentRoot } from '../lib/contentRoot';
|
|
16
|
+
import { createReadEntryTool, createGroveMetadataTool, groveAgentTools, toolExecutor } from '../lib/agentTools';
|
|
17
|
+
import { buildSystemPrompt } from '../lib/agentPrompt';
|
|
18
|
+
import { computeReachRows, reachChips, showEgressDisclosure, EGRESS_DISCLOSURE } from '../lib/reachCard';
|
|
19
|
+
import { transcriptToRows, toolActivityLine, type AgentRow } from '../lib/agentTranscript';
|
|
20
|
+
import { safeSources } from '../lib/safeSources';
|
|
5
21
|
import Icon from './Icon';
|
|
6
22
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
23
|
+
// `.grove-agent` — Grove's own embedded agent (GROVE_AGENT_SPEC).
|
|
24
|
+
//
|
|
25
|
+
// The surface is a FUNCTION of the session's envelope (R-GA-1): the reach card in
|
|
26
|
+
// the expanded header is computed from the provider three-state, the `llm:chat`
|
|
27
|
+
// grant (the grant-filtered catalog), mount writability, and source trust — never
|
|
28
|
+
// hand-written copy. The loop rides the workbench's seam — SDK `runAgent` over the
|
|
29
|
+
// host `llm.chat` slot — and its two tools are the mount-chrooted `read_entry` and
|
|
30
|
+
// the index query (R-GA-2). The widget never writes (R-GA-3): every change is a
|
|
31
|
+
// hand-off to the editor / workbench. Read-only never blocks Q&A (R-GA-5). When a
|
|
32
|
+
// provider is bound the egress line is shown unconditionally (R-GA-6). Every
|
|
33
|
+
// corpus-derived byte entering the loop is fenced (R-GA-7).
|
|
34
|
+
export default function GroveAgent({
|
|
35
|
+
writable,
|
|
36
|
+
entryKey,
|
|
37
|
+
entryTitle,
|
|
38
|
+
}: {
|
|
39
|
+
writable: boolean;
|
|
40
|
+
entryKey: string;
|
|
41
|
+
entryTitle: string;
|
|
42
|
+
}) {
|
|
43
|
+
const providerState = useChatProviderState();
|
|
44
|
+
const catalog = useCatalog();
|
|
45
|
+
const index = useAllMetadata();
|
|
46
|
+
const headings = useHeadings(entryKey);
|
|
47
|
+
const activeHeading = useActiveHeading(headings);
|
|
48
|
+
const { openEditor, editRefused } = useShell();
|
|
31
49
|
const [open, setOpen] = useState(false);
|
|
32
50
|
const [detent, setDetent] = useState<'half' | 'full'>('half');
|
|
33
51
|
const [resting, setResting] = useState('');
|
|
34
52
|
const [draft, setDraft] = useState('');
|
|
35
|
-
const [
|
|
53
|
+
const [rows, setRows] = useState<AgentRow[]>([]);
|
|
36
54
|
const [streaming, setStreaming] = useState(false);
|
|
37
|
-
const [
|
|
55
|
+
const [errorToast, setErrorToast] = useState<string | null>(null);
|
|
38
56
|
const bodyRef = useRef<HTMLDivElement>(null);
|
|
39
57
|
const footRef = useRef<HTMLInputElement>(null);
|
|
58
|
+
// One AbortController per run (R3-608): the composer's stop control aborts the
|
|
59
|
+
// in-flight run through the SDK signal the loop already honours.
|
|
60
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
61
|
+
// The panel carries the dialog contract (R3-608): focus in, Tab trapped,
|
|
62
|
+
// Escape, focus return — the shared hook, stack-aware.
|
|
63
|
+
const dialogRef = useOverlayFocusDismiss(open, () => setOpen(false));
|
|
64
|
+
|
|
65
|
+
// The envelope, computed (R-GA-1). `llm:chat` appears in the grant-filtered
|
|
66
|
+
// catalog iff this app holds the consent — an ungranted fork reads a DISTINCT
|
|
67
|
+
// cause from a user without a key (G-GA-10).
|
|
68
|
+
const chatGranted = catalog.some((m) => m.name === 'llm:chat');
|
|
69
|
+
const context = useAgentContext({ entryPath: entryKey, entryTitle, heading: activeHeading || undefined });
|
|
70
|
+
const reachRows = useMemo(
|
|
71
|
+
() => computeReachRows({ providerState, chatGranted, writable, sourceShared: context.sourceShared }),
|
|
72
|
+
[providerState, chatGranted, writable, context.sourceShared],
|
|
73
|
+
);
|
|
74
|
+
const chips = useMemo(() => reachChips(reachRows), [reachRows]);
|
|
75
|
+
const canAsk = providerState.status === 'configured' && chatGranted;
|
|
76
|
+
|
|
77
|
+
// Read at CALL time (a scan may land, the reader may navigate) — refs kept fresh
|
|
78
|
+
// in an effect, never written during render (the react-hooks compiler rejects that).
|
|
79
|
+
const indexRef = useRef(index);
|
|
80
|
+
const contextRef = useRef(context);
|
|
81
|
+
const entryRef = useRef(entryKey);
|
|
82
|
+
const headingRef = useRef(activeHeading);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
indexRef.current = index;
|
|
85
|
+
contextRef.current = context;
|
|
86
|
+
entryRef.current = entryKey;
|
|
87
|
+
headingRef.current = activeHeading;
|
|
88
|
+
}, [index, context, entryKey, activeHeading]);
|
|
40
89
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const staticBanner: Banner = !writable ? 'read-only' : provider === null ? 'no-key' : null;
|
|
44
|
-
const banner: Banner = sendBanner || staticBanner;
|
|
90
|
+
// The loop's transcript, kept so follow-up questions replay the tool-use chain.
|
|
91
|
+
const transcriptRef = useRef<AgentMessage[]>([]);
|
|
45
92
|
|
|
46
93
|
useEffect(() => {
|
|
47
94
|
if (open) requestAnimationFrame(() => footRef.current?.focus());
|
|
48
95
|
}, [open]);
|
|
96
|
+
// The panel's only trigger (the resting input) UNMOUNTS when it opens, and
|
|
97
|
+
// its onFocus IS the open action — returning focus to it would reopen the
|
|
98
|
+
// panel. So the close path returns focus to the resting line's SUBMIT
|
|
99
|
+
// control (R-IX-1): a real stop in the same surface that never reopens
|
|
100
|
+
// anything, so Escape never strands the keyboard on <body>. On mount the
|
|
101
|
+
// effect stays quiet (was-open gate) or the panel would open itself.
|
|
102
|
+
const restingGoRef = useRef<HTMLButtonElement>(null);
|
|
103
|
+
const wasOpenRef = useRef(false);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (wasOpenRef.current && !open) requestAnimationFrame(() => restingGoRef.current?.focus());
|
|
106
|
+
wasOpenRef.current = open;
|
|
107
|
+
}, [open]);
|
|
49
108
|
useEffect(() => {
|
|
50
109
|
bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight });
|
|
51
|
-
}, [
|
|
110
|
+
}, [rows, streaming]);
|
|
52
111
|
|
|
53
|
-
async function
|
|
54
|
-
const q =
|
|
112
|
+
async function ask(question: string) {
|
|
113
|
+
const q = question.trim();
|
|
55
114
|
if (!q || streaming) return;
|
|
56
115
|
setOpen(true);
|
|
57
116
|
setDraft('');
|
|
58
117
|
setResting('');
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
setErrorToast(null);
|
|
119
|
+
setRows((prev) => [...prev, { kind: 'user', text: q }]);
|
|
61
120
|
setStreaming(true);
|
|
62
|
-
setSendBanner(null);
|
|
63
121
|
|
|
122
|
+
// G-GA-8: a provider without `features.tools` degrades to context-stuffing —
|
|
123
|
+
// the deixis block, an index summary, and the current entry body ride the
|
|
124
|
+
// prompt; the request carries ZERO tools.
|
|
125
|
+
const toolsSupported = providerState.status === 'configured' && providerState.provider.features.tools === true;
|
|
126
|
+
const chroot = getContentRoot();
|
|
127
|
+
const currentKey = entryRef.current;
|
|
128
|
+
const contextBlock = renderAgentContext({
|
|
129
|
+
...contextRef.current,
|
|
130
|
+
entryPath: currentKey,
|
|
131
|
+
heading: headingRef.current || undefined,
|
|
132
|
+
});
|
|
133
|
+
let stuffedBody: string | undefined;
|
|
134
|
+
if (!toolsSupported) {
|
|
135
|
+
stuffedBody = await safeSources.read(currentKey).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
const system = buildSystemPrompt({
|
|
138
|
+
contextBlock,
|
|
139
|
+
toolsSupported,
|
|
140
|
+
...(toolsSupported ? {} : { entryBody: stuffedBody, entryPath: currentKey, index: indexRef.current, chroot }),
|
|
141
|
+
});
|
|
142
|
+
const entryTool = createReadEntryTool(chroot);
|
|
143
|
+
const metaTool = createGroveMetadataTool(chroot, () => indexRef.current);
|
|
144
|
+
const execute = toolExecutor([entryTool, metaTool]);
|
|
145
|
+
const tools = toolsSupported ? groveAgentTools(chroot, () => indexRef.current) : [];
|
|
146
|
+
|
|
147
|
+
let acc = '';
|
|
148
|
+
const pushAssistant = (text: string) =>
|
|
149
|
+
setRows((prev) => {
|
|
150
|
+
const next = prev.slice();
|
|
151
|
+
if (next.length && next[next.length - 1].kind === 'assistant') next[next.length - 1] = { kind: 'assistant', text };
|
|
152
|
+
else next.push({ kind: 'assistant', text });
|
|
153
|
+
return next;
|
|
154
|
+
});
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
abortRef.current = controller;
|
|
64
157
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
158
|
+
const final = await runAgent({
|
|
159
|
+
client: createChatModelClient(),
|
|
160
|
+
tools,
|
|
161
|
+
execute,
|
|
162
|
+
system,
|
|
163
|
+
prompt: q,
|
|
164
|
+
history: transcriptRef.current,
|
|
165
|
+
maxTurns: 8,
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
events: {
|
|
168
|
+
onAssistantDelta: (t) => {
|
|
169
|
+
acc += t;
|
|
170
|
+
pushAssistant(acc);
|
|
171
|
+
},
|
|
172
|
+
onToolUse: (name, input) => {
|
|
173
|
+
acc = '';
|
|
174
|
+
setRows((prev) => {
|
|
175
|
+
const next = prev.slice();
|
|
176
|
+
if (next.length && next[next.length - 1].kind === 'assistant' && !next[next.length - 1].text) next.pop();
|
|
177
|
+
next.push({ kind: 'activity', text: toolActivityLine(name, input) });
|
|
178
|
+
return next;
|
|
179
|
+
});
|
|
75
180
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const rendered = transcriptToRows(final);
|
|
184
|
+
// The real loop RESOLVES an aborted run with the partial transcript (a
|
|
185
|
+
// clean stop, never a thrown error) — so the abort check lives here too,
|
|
186
|
+
// not only in the catch: a stopped run keeps what streamed and appends a
|
|
187
|
+
// named Stopped row (R3-608).
|
|
188
|
+
if (controller.signal.aborted) {
|
|
189
|
+
setRows((prev) => [...prev, { kind: 'activity', text: 'Stopped' }]);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!rendered.some((r) => r.kind === 'assistant')) throw new Error('empty');
|
|
193
|
+
transcriptRef.current = final.slice();
|
|
194
|
+
setRows(rendered);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// A stop is not an error (R3-608): the aborted run settles with a named
|
|
197
|
+
// "Stopped" row, and the transcript keeps what streamed so far.
|
|
198
|
+
if (controller.signal.aborted) {
|
|
199
|
+
setRows((prev) => [...prev, { kind: 'activity', text: 'Stopped' }]);
|
|
200
|
+
return;
|
|
93
201
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
202
|
+
const code = (e as { code?: string })?.code || (e as Error).message;
|
|
203
|
+
setErrorToast(
|
|
204
|
+
code === 'auth-required'
|
|
205
|
+
? 'no model key connected — add one in Settings'
|
|
206
|
+
: code === 'forbidden'
|
|
207
|
+
? "this Grove wasn't granted chat — reading works as normal"
|
|
208
|
+
: 'the model or backend errored — try again in a moment',
|
|
209
|
+
);
|
|
210
|
+
setRows((prev) =>
|
|
211
|
+
prev.length && prev[prev.length - 1].kind === 'assistant' && !prev[prev.length - 1].text ? prev.slice(0, -1) : prev,
|
|
212
|
+
);
|
|
100
213
|
} finally {
|
|
101
214
|
setStreaming(false);
|
|
215
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
102
216
|
}
|
|
103
217
|
}
|
|
104
218
|
|
|
105
|
-
const restingDisabled = banner === 'read-only';
|
|
106
|
-
|
|
107
219
|
return (
|
|
108
220
|
<div className="grove-agent">
|
|
109
|
-
{/* Resting: the always-present thin input line
|
|
221
|
+
{/* Resting: the always-present thin input line. Q&A is never disabled by
|
|
222
|
+
read-only (R-GA-5) — only by the envelope genuinely lacking a provider. */}
|
|
110
223
|
{!open && (
|
|
111
224
|
<form
|
|
112
225
|
className="ga-line"
|
|
113
226
|
onSubmit={(e) => {
|
|
114
227
|
e.preventDefault();
|
|
115
|
-
if (
|
|
116
|
-
else
|
|
228
|
+
if (canAsk) void ask(resting);
|
|
229
|
+
else setOpen(true);
|
|
117
230
|
}}
|
|
118
231
|
>
|
|
119
232
|
<span className="mk" />
|
|
120
233
|
<input
|
|
121
234
|
value={resting}
|
|
122
|
-
placeholder={
|
|
235
|
+
placeholder={`Ask Grove about “${entryTitle}”…`}
|
|
123
236
|
onChange={(e) => setResting(e.target.value)}
|
|
124
237
|
onFocus={() => setOpen(true)}
|
|
125
238
|
aria-label="Ask Grove"
|
|
126
239
|
/>
|
|
127
|
-
<button className="go" type="submit" aria-label="Ask Grove">
|
|
240
|
+
<button ref={restingGoRef} className="go" type="submit" aria-label="Ask Grove">
|
|
128
241
|
<Icon name="send" />
|
|
129
242
|
</button>
|
|
130
243
|
</form>
|
|
@@ -134,16 +247,20 @@ export default function GroveAgent({ writable, entryKey, entryTitle }: { writabl
|
|
|
134
247
|
{open && (
|
|
135
248
|
<>
|
|
136
249
|
<div className="ga-scrim" onClick={() => setOpen(false)} />
|
|
137
|
-
<div
|
|
250
|
+
<div
|
|
251
|
+
className="ga-panel"
|
|
252
|
+
data-detent={detent}
|
|
253
|
+
ref={dialogRef}
|
|
254
|
+
tabIndex={-1}
|
|
255
|
+
role="dialog"
|
|
256
|
+
aria-modal="true"
|
|
257
|
+
aria-label="Ask Grove"
|
|
258
|
+
>
|
|
138
259
|
<div className="ga-panel-inner">
|
|
139
260
|
<div className="ga-head">
|
|
140
261
|
<span className="grip" onClick={() => setDetent((d) => (d === 'half' ? 'full' : 'half'))} />
|
|
141
262
|
<span className="mk" />
|
|
142
263
|
<span className="id">Grove</span>
|
|
143
|
-
<span className="scope">
|
|
144
|
-
<Icon name="shield" />
|
|
145
|
-
scoped to this wiki
|
|
146
|
-
</span>
|
|
147
264
|
<span className="grow">
|
|
148
265
|
<button className="ic" aria-label={detent === 'half' ? 'Expand' : 'Collapse'} onClick={() => setDetent((d) => (d === 'half' ? 'full' : 'half'))}>
|
|
149
266
|
<Icon name={detent === 'half' ? 'chevron-down' : 'chevron-right'} />
|
|
@@ -154,19 +271,36 @@ export default function GroveAgent({ writable, entryKey, entryTitle }: { writabl
|
|
|
154
271
|
</span>
|
|
155
272
|
</div>
|
|
156
273
|
|
|
157
|
-
{
|
|
274
|
+
{/* The reach card — the envelope, computed (R-GA-1). */}
|
|
275
|
+
<div className="ga-reach" role="list" aria-label="What the agent can do here">
|
|
276
|
+
{reachRows.map((r) => (
|
|
277
|
+
<div className={`ga-reach__row ga-reach__row--${r.state}`} role="listitem" key={r.key}>
|
|
278
|
+
<span className="ga-reach__mark" aria-hidden>
|
|
279
|
+
{r.state === 'ok' ? '✓' : r.state === 'blocked' ? '✗' : '·'}
|
|
280
|
+
</span>
|
|
281
|
+
<span className="ga-reach__label">{r.label}</span>
|
|
282
|
+
{r.cause && <span className="ga-reach__cause">{r.cause}</span>}
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
{showEgressDisclosure(providerState) && <p className="ga-egress">{EGRESS_DISCLOSURE}</p>}
|
|
287
|
+
{errorToast && (
|
|
288
|
+
<div className="ga-toast" role="status">
|
|
289
|
+
{errorToast}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
158
292
|
|
|
159
293
|
<div className="ga-body" ref={bodyRef}>
|
|
160
|
-
{
|
|
294
|
+
{rows.length === 0 ? (
|
|
161
295
|
<>
|
|
162
296
|
<div className="ga-explain">
|
|
163
|
-
<b>Grove's agent</b>
|
|
164
|
-
|
|
297
|
+
<b>Grove's agent</b> answers questions about this wiki — its entries, tags, and structure — from the
|
|
298
|
+
page you're on.
|
|
165
299
|
</div>
|
|
166
|
-
{
|
|
300
|
+
{chips.length > 0 && (
|
|
167
301
|
<div className="ga-chips">
|
|
168
|
-
{
|
|
169
|
-
<button key={c} className="ga-chip" onClick={() =>
|
|
302
|
+
{chips.map((c) => (
|
|
303
|
+
<button key={c} className="ga-chip" onClick={() => void ask(c)}>
|
|
170
304
|
{c}
|
|
171
305
|
</button>
|
|
172
306
|
))}
|
|
@@ -174,20 +308,27 @@ export default function GroveAgent({ writable, entryKey, entryTitle }: { writabl
|
|
|
174
308
|
)}
|
|
175
309
|
</>
|
|
176
310
|
) : (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
311
|
+
rows.map((r, i) =>
|
|
312
|
+
r.kind === 'activity' ? (
|
|
313
|
+
<div key={i} className="ga-activity">
|
|
314
|
+
<Icon name="file" />
|
|
315
|
+
{r.text}
|
|
316
|
+
</div>
|
|
317
|
+
) : (
|
|
318
|
+
<div key={i} className={`ga-msg ${r.kind}`}>
|
|
319
|
+
<span className={`ga-msg__av ${r.kind === 'assistant' ? 'grove' : 'user'}`}>
|
|
320
|
+
{r.kind === 'assistant' ? '' : 'you'}
|
|
321
|
+
</span>
|
|
322
|
+
<div className="ga-msg__b">
|
|
323
|
+
<div className="ga-msg__who">{r.kind === 'assistant' ? 'Grove' : 'You'}</div>
|
|
324
|
+
<div className="ga-msg__txt">
|
|
325
|
+
{r.text}
|
|
326
|
+
{streaming && i === rows.length - 1 && r.kind === 'assistant' ? <span className="ga-cursor" /> : null}
|
|
327
|
+
</div>
|
|
187
328
|
</div>
|
|
188
329
|
</div>
|
|
189
|
-
|
|
190
|
-
)
|
|
330
|
+
),
|
|
331
|
+
)
|
|
191
332
|
)}
|
|
192
333
|
</div>
|
|
193
334
|
|
|
@@ -196,30 +337,42 @@ export default function GroveAgent({ writable, entryKey, entryTitle }: { writabl
|
|
|
196
337
|
className="ga-foot__row"
|
|
197
338
|
onSubmit={(e) => {
|
|
198
339
|
e.preventDefault();
|
|
199
|
-
|
|
340
|
+
void ask(draft);
|
|
200
341
|
}}
|
|
201
342
|
>
|
|
202
343
|
<input
|
|
203
344
|
ref={footRef}
|
|
204
345
|
value={draft}
|
|
205
346
|
placeholder="Ask Grove…"
|
|
206
|
-
disabled={streaming ||
|
|
347
|
+
disabled={streaming || !canAsk}
|
|
207
348
|
onChange={(e) => setDraft(e.target.value)}
|
|
208
349
|
/>
|
|
209
|
-
|
|
210
|
-
<
|
|
350
|
+
{streaming ? (
|
|
351
|
+
<button
|
|
352
|
+
className="stop"
|
|
353
|
+
type="button"
|
|
354
|
+
aria-label="Stop"
|
|
355
|
+
title="Stop"
|
|
356
|
+
onClick={() => abortRef.current?.abort()}
|
|
357
|
+
>
|
|
358
|
+
<Icon name="x" />
|
|
359
|
+
</button>
|
|
360
|
+
) : null}
|
|
361
|
+
<button className="go" type="submit" disabled={streaming || !canAsk || !draft.trim()} aria-label={streaming ? 'Answering…' : 'Send'} title={streaming ? 'Answering…' : 'Send'}>
|
|
362
|
+
<Icon name="send" />
|
|
211
363
|
</button>
|
|
212
364
|
</form>
|
|
213
365
|
<div className="ga-foot__hand">
|
|
214
|
-
<span>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
366
|
+
<span>{EGRESS_DISCLOSURE}</span>
|
|
367
|
+
{editRefused && (
|
|
368
|
+
<span className="ga-edit-refused" role="status">
|
|
369
|
+
{EDIT_REFUSED_NOTICE}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
<button type="button" onClick={() => openEditor(entryKey)}>
|
|
220
373
|
<Icon name="external" />
|
|
221
|
-
Open in the
|
|
222
|
-
</
|
|
374
|
+
Open in the editor
|
|
375
|
+
</button>
|
|
223
376
|
</div>
|
|
224
377
|
</div>
|
|
225
378
|
</div>
|
|
@@ -229,37 +382,3 @@ export default function GroveAgent({ writable, entryKey, entryTitle }: { writabl
|
|
|
229
382
|
</div>
|
|
230
383
|
);
|
|
231
384
|
}
|
|
232
|
-
|
|
233
|
-
// The no-key / forbidden / read-only / error states — calm explanations that hand
|
|
234
|
-
// to the host secret flow rather than faking a key prompt.
|
|
235
|
-
function AgentBanner({ banner }: { banner: Banner }) {
|
|
236
|
-
const copy: Record<string, { t: string; d: React.ReactNode }> = {
|
|
237
|
-
'no-key': {
|
|
238
|
-
t: 'Connect a key to use the agent.',
|
|
239
|
-
d: 'Grove rides whichever model provider you connect in the host. Add a key to start asking.',
|
|
240
|
-
},
|
|
241
|
-
forbidden: {
|
|
242
|
-
t: 'The agent isn’t available here.',
|
|
243
|
-
d: "This Grove wasn't granted the chat capability. Reading still works as normal.",
|
|
244
|
-
},
|
|
245
|
-
'read-only': {
|
|
246
|
-
t: 'You’re viewing read-only.',
|
|
247
|
-
d: 'The agent can explain this wiki, but changes need write access to this space.',
|
|
248
|
-
},
|
|
249
|
-
error: {
|
|
250
|
-
t: 'The agent hit a snag.',
|
|
251
|
-
d: 'The model or backend errored. Try again in a moment.',
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
const c = copy[banner as string];
|
|
255
|
-
if (!c) return null;
|
|
256
|
-
return (
|
|
257
|
-
<div className="ga-banner">
|
|
258
|
-
<Icon name="alert" />
|
|
259
|
-
<div>
|
|
260
|
-
<div className="ga-banner__t">{c.t}</div>
|
|
261
|
-
<div className="ga-banner__d">{c.d}</div>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
}
|