@opengeni/react 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/artifacts.js +2 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ERGYJIBB.js → chunk-AWZLIMKX.js} +2 -2
- package/dist/{chunk-ERGYJIBB.js.map → chunk-AWZLIMKX.js.map} +1 -1
- package/dist/{chunk-FZCP527Y.js → chunk-CJUO7J5Z.js} +2 -2
- package/dist/{chunk-742CZ3L3.js → chunk-H5X55XH7.js} +16 -200
- package/dist/chunk-H5X55XH7.js.map +1 -0
- package/dist/{chunk-PGSX7UCP.js → chunk-LJ2UTJJE.js} +1369 -785
- package/dist/chunk-LJ2UTJJE.js.map +1 -0
- package/dist/{chunk-CY7WK5FD.js → chunk-LN6TCXD3.js} +22 -1
- package/dist/chunk-LN6TCXD3.js.map +1 -0
- package/dist/{chunk-TMYGL6X5.js → chunk-MBMPHOMA.js} +71 -6
- package/dist/chunk-MBMPHOMA.js.map +1 -0
- package/dist/{chunk-RC6BZ44Q.js → chunk-OA6HFRQP.js} +2 -2
- package/dist/{chunk-WWEUO2PL.js → chunk-WZMYFJD3.js} +190 -11
- package/dist/chunk-WZMYFJD3.js.map +1 -0
- package/dist/components/composer-transcription-control.d.ts +3 -0
- package/dist/components/open-geni-chat.d.ts +6 -2
- package/dist/components/session-conversation.d.ts +25 -19
- package/dist/components/session-list.d.ts +4 -1
- package/dist/components/tool-action-review.d.ts +29 -2
- package/dist/components/tool-review-history.d.ts +8 -4
- package/dist/composer.js +1 -1
- package/dist/connect.js +1 -1
- package/dist/hooks/use-client-config-flags.d.ts +37 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +21 -11
- package/dist/index.js.map +1 -1
- package/dist/{latest-question-YTOBHCWR.js → latest-question-GPYM2JNU.js} +3 -3
- package/dist/realtime/realtime-control.d.ts +2 -0
- package/dist/realtime.js +25 -4
- package/dist/realtime.js.map +1 -1
- package/dist/session-capability-policy.d.ts +20 -1
- package/dist/session-ui.js +6 -6
- package/dist/session.js +6 -6
- package/dist/timeline/index.d.ts +2 -0
- package/dist/timeline/retained-loaders.d.ts +27 -0
- package/package.json +3 -3
- package/src/components/approval-surface.tsx +190 -79
- package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
- package/src/components/composer-transcription-control.tsx +14 -0
- package/src/components/composer.tsx +1 -0
- package/src/components/open-geni-chat.tsx +53 -13
- package/src/components/session-conversation.tsx +229 -63
- package/src/components/session-list.tsx +9 -2
- package/src/components/tool-action-review.tsx +502 -239
- package/src/components/tool-review-history.tsx +18 -43
- package/src/hooks/use-client-config-flags.ts +102 -0
- package/src/hooks/use-session-events.ts +19 -16
- package/src/hooks/use-transcription.ts +6 -0
- package/src/hooks/use-voice-input.ts +6 -0
- package/src/index.ts +9 -0
- package/src/realtime/realtime-control.tsx +28 -1
- package/src/session-capability-policy.ts +119 -6
- package/src/timeline/index.ts +10 -0
- package/src/timeline/retained-loaders.ts +86 -0
- package/src/timeline/tool-display-name.ts +5 -1
- package/src/timeline/tool-renderers.tsx +143 -10
- package/styles/compiled.css +65 -21
- package/styles/tokens.css +1 -1
- package/dist/chunk-742CZ3L3.js.map +0 -1
- package/dist/chunk-CY7WK5FD.js.map +0 -1
- package/dist/chunk-PGSX7UCP.js.map +0 -1
- package/dist/chunk-TMYGL6X5.js.map +0 -1
- package/dist/chunk-WWEUO2PL.js.map +0 -1
- /package/dist/{chunk-FZCP527Y.js.map → chunk-CJUO7J5Z.js.map} +0 -0
- /package/dist/{chunk-RC6BZ44Q.js.map → chunk-OA6HFRQP.js.map} +0 -0
- /package/dist/{latest-question-YTOBHCWR.js.map → latest-question-GPYM2JNU.js.map} +0 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniApiError,
|
|
3
|
+
type FileResourceRef,
|
|
4
|
+
type LatencyMode,
|
|
5
|
+
type OpenGeniClient,
|
|
6
|
+
type ReasoningEffort,
|
|
7
|
+
} from "@opengeni/sdk";
|
|
2
8
|
import { MenuIcon, XIcon } from "lucide-react";
|
|
3
9
|
import { useCallback, useRef, useState, type CSSProperties, type FormEvent } from "react";
|
|
4
10
|
import { useWorkspaceModelCatalog } from "../hooks/use-available-models";
|
|
@@ -14,9 +20,10 @@ import {
|
|
|
14
20
|
import { useOpenGeni, type ClientOverride } from "../session-context";
|
|
15
21
|
import { ChatComposer } from "./chat-composer";
|
|
16
22
|
import { ModelPolicyPicker } from "./model-policy-picker";
|
|
23
|
+
import { useClientConfigFlags } from "../hooks/use-client-config-flags";
|
|
17
24
|
import {
|
|
18
25
|
SessionConversation,
|
|
19
|
-
|
|
26
|
+
useComposerTranscription,
|
|
20
27
|
type SessionConversationProps,
|
|
21
28
|
} from "./session-conversation";
|
|
22
29
|
import { SessionList, type SessionListLabels, type SessionListProps } from "./session-list";
|
|
@@ -71,7 +78,11 @@ export type OpenGeniChatProps = ClientOverride &
|
|
|
71
78
|
options: OpenGeniChatCreateOptions,
|
|
72
79
|
) => Promise<string>)
|
|
73
80
|
| undefined;
|
|
74
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Hide the "New chat" entry point. It is also hidden when the session proxy
|
|
83
|
+
* reports chat creation unavailable (no `createSession` hook) and no
|
|
84
|
+
* `createSession` prop is given.
|
|
85
|
+
*/
|
|
75
86
|
newChat?: boolean | undefined;
|
|
76
87
|
/**
|
|
77
88
|
* Forwarded to the conversation (message rendering, tool renderers,
|
|
@@ -152,6 +163,9 @@ function Chat({
|
|
|
152
163
|
const [listRevision, setListRevision] = useState(0);
|
|
153
164
|
const root = useRef<HTMLDivElement>(null);
|
|
154
165
|
const hostTheme = useHostTheme(root, { theme, surface });
|
|
166
|
+
const config = useClientConfigFlags(context.client);
|
|
167
|
+
// A host creator replaces the proxy route; otherwise trust the proxy's report.
|
|
168
|
+
const canCreate = createSession !== undefined || config.sessionCreation;
|
|
155
169
|
|
|
156
170
|
const select = useCallback(
|
|
157
171
|
(next: string | null) => {
|
|
@@ -171,12 +185,20 @@ function Chat({
|
|
|
171
185
|
if (createSession) return await createSession(initialMessage, idempotencyKey, options);
|
|
172
186
|
const creator = (context.client as unknown as CreateClient).createSession;
|
|
173
187
|
if (typeof creator !== "function") throw new Error(labels.newChatUnavailable);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
try {
|
|
189
|
+
const created = await creator.call(context.client, context.workspaceId, {
|
|
190
|
+
initialMessage,
|
|
191
|
+
idempotencyKey,
|
|
192
|
+
...options,
|
|
193
|
+
} as Parameters<NonNullable<CreateClient["createSession"]>>[1]);
|
|
194
|
+
return created.id;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// A proxy without a createSession hook refuses the route itself.
|
|
197
|
+
if (error instanceof OpenGeniApiError && error.code === "route_not_allowed") {
|
|
198
|
+
throw new Error(labels.newChatUnavailable, { cause: error });
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
180
202
|
},
|
|
181
203
|
[context.client, context.workspaceId, createSession, labels.newChatUnavailable],
|
|
182
204
|
);
|
|
@@ -188,7 +210,7 @@ function Chat({
|
|
|
188
210
|
labels={labels}
|
|
189
211
|
selectedSessionId={selected}
|
|
190
212
|
onSelect={select}
|
|
191
|
-
onNewChat={newChat ? () => select(null) : undefined}
|
|
213
|
+
onNewChat={newChat && canCreate ? () => select(null) : undefined}
|
|
192
214
|
onArchived={(archivedId) => {
|
|
193
215
|
if (archivedId === selected) select(null);
|
|
194
216
|
}}
|
|
@@ -256,6 +278,8 @@ function Chat({
|
|
|
256
278
|
{selected ? (
|
|
257
279
|
<SessionConversation
|
|
258
280
|
{...conversationProps}
|
|
281
|
+
// Sub-agent chats open in place, like a chat from the list.
|
|
282
|
+
onOpenSession={conversationProps?.onOpenSession ?? select}
|
|
259
283
|
{...scope}
|
|
260
284
|
sessionId={selected}
|
|
261
285
|
height="100%"
|
|
@@ -266,6 +290,7 @@ function Chat({
|
|
|
266
290
|
labels={labels}
|
|
267
291
|
placeholderOverride={labelOverrides?.newChatPlaceholder}
|
|
268
292
|
conversationProps={conversationProps}
|
|
293
|
+
available={canCreate}
|
|
269
294
|
create={create}
|
|
270
295
|
onCreated={(id) => {
|
|
271
296
|
setListRevision((revision) => revision + 1);
|
|
@@ -291,6 +316,7 @@ function NewChat({
|
|
|
291
316
|
labels,
|
|
292
317
|
placeholderOverride,
|
|
293
318
|
conversationProps,
|
|
319
|
+
available,
|
|
294
320
|
create,
|
|
295
321
|
onCreated,
|
|
296
322
|
}: {
|
|
@@ -298,6 +324,8 @@ function NewChat({
|
|
|
298
324
|
labels: OpenGeniChatLabels;
|
|
299
325
|
placeholderOverride: string | undefined;
|
|
300
326
|
conversationProps: OpenGeniChatProps["conversationProps"];
|
|
327
|
+
/** False when this product cannot start chats; the composer is disabled and says so. */
|
|
328
|
+
available: boolean;
|
|
301
329
|
create: (
|
|
302
330
|
initialMessage: string,
|
|
303
331
|
idempotencyKey: string,
|
|
@@ -317,6 +345,11 @@ function NewChat({
|
|
|
317
345
|
});
|
|
318
346
|
const files = useFileAttachments(scope);
|
|
319
347
|
const uploadsEnabled = (conversationProps?.attachments ?? true) && config.uploads;
|
|
348
|
+
const transcription = useComposerTranscription(
|
|
349
|
+
context.client,
|
|
350
|
+
context.workspaceId,
|
|
351
|
+
(conversationProps?.voiceInput ?? true) && available ? config.voiceInput : null,
|
|
352
|
+
);
|
|
320
353
|
const [text, setText] = useState("");
|
|
321
354
|
const [sending, setSending] = useState(false);
|
|
322
355
|
const [error, setError] = useState<{ cause: unknown } | null>(null);
|
|
@@ -333,7 +366,7 @@ function NewChat({
|
|
|
333
366
|
|
|
334
367
|
const submit = async (): Promise<boolean> => {
|
|
335
368
|
const initialMessage = text.trim() || (resources.length > 0 ? FILE_ONLY_MESSAGE_TEXT : "");
|
|
336
|
-
if (!initialMessage || sending || pendingFiles) return false;
|
|
369
|
+
if (!initialMessage || sending || pendingFiles || !available) return false;
|
|
337
370
|
const options: OpenGeniChatCreateOptions = {
|
|
338
371
|
...(resources.length > 0 ? { resources } : {}),
|
|
339
372
|
...(showModelPicker ? choice : {}),
|
|
@@ -377,7 +410,8 @@ function NewChat({
|
|
|
377
410
|
send: submit,
|
|
378
411
|
steer: submit,
|
|
379
412
|
sending,
|
|
380
|
-
canSend:
|
|
413
|
+
canSend:
|
|
414
|
+
available && (text.trim().length > 0 || resources.length > 0) && !sending && !pendingFiles,
|
|
381
415
|
pause: NOOP_ASYNC,
|
|
382
416
|
pausing: false,
|
|
383
417
|
resume: NOOP_ASYNC,
|
|
@@ -420,8 +454,10 @@ function NewChat({
|
|
|
420
454
|
) : null}
|
|
421
455
|
<ChatComposer
|
|
422
456
|
{...composerProps}
|
|
457
|
+
{...(transcription && composerProps?.transcription === undefined ? { transcription } : {})}
|
|
423
458
|
composer={composer}
|
|
424
459
|
attachments={uploadsEnabled ? files : undefined}
|
|
460
|
+
disabled={!available || composerProps?.disabled}
|
|
425
461
|
runControl="none"
|
|
426
462
|
running={false}
|
|
427
463
|
placeholder={placeholderOverride ?? composerProps?.placeholder ?? labels.newChatPlaceholder}
|
|
@@ -455,7 +491,11 @@ function NewChat({
|
|
|
455
491
|
}
|
|
456
492
|
responsiveBasis={composerProps?.responsiveBasis ?? "container"}
|
|
457
493
|
/>
|
|
458
|
-
{
|
|
494
|
+
{!available ? (
|
|
495
|
+
<p className="text-center text-og-sm text-og-fg-muted" data-og-new-chat-unavailable="">
|
|
496
|
+
{labels.newChatUnavailable}
|
|
497
|
+
</p>
|
|
498
|
+
) : error ? (
|
|
459
499
|
<p role="alert" className="text-center text-og-sm text-og-status-failed">
|
|
460
500
|
{formatError(
|
|
461
501
|
error.cause,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
resolveWorkspaceVoiceInputEnabled,
|
|
3
|
+
SESSION_SCOPE_HEADER,
|
|
4
|
+
type ClientVoiceInputConfig,
|
|
5
|
+
type OpenGeniClient,
|
|
6
|
+
type SendMessageInput,
|
|
7
|
+
} from "@opengeni/sdk";
|
|
2
8
|
import {
|
|
3
9
|
lazy,
|
|
4
10
|
Suspense,
|
|
@@ -7,6 +13,7 @@ import {
|
|
|
7
13
|
useMemo,
|
|
8
14
|
useRef,
|
|
9
15
|
useState,
|
|
16
|
+
type ComponentProps,
|
|
10
17
|
type CSSProperties,
|
|
11
18
|
} from "react";
|
|
12
19
|
import type { SiteSnapshotClient } from "./artifacts/chat-interactive-block";
|
|
@@ -20,9 +27,17 @@ import { useComposer } from "../hooks/use-composer";
|
|
|
20
27
|
import { useHumanInputRequests } from "../hooks/use-human-input";
|
|
21
28
|
import { useFileAttachments } from "../hooks/use-file-attachments";
|
|
22
29
|
import { useSessionControl } from "../hooks/use-session-control";
|
|
30
|
+
import { useGoal, type UseGoalOptions } from "../hooks/use-goal";
|
|
31
|
+
import { useClientConfigFlags } from "../hooks/use-client-config-flags";
|
|
32
|
+
import {
|
|
33
|
+
createSessionRetainedScreenshotLoader,
|
|
34
|
+
createWorkspaceRetainedArtifactLoader,
|
|
35
|
+
createWorkspaceRetainedVideoLoader,
|
|
36
|
+
} from "../timeline/retained-loaders";
|
|
23
37
|
import { projectPendingApprovals } from "../approvals";
|
|
24
38
|
import { ApprovalSurface } from "./approval-surface";
|
|
25
39
|
import { ChatComposer, type ChatComposerProps } from "./chat-composer";
|
|
40
|
+
import type { ComposerTranscriptionControlProps } from "./composer-transcription-control";
|
|
26
41
|
import { SessionChrome } from "./session-chrome";
|
|
27
42
|
import { HumanInputSurface, type HumanInputSurfaceProps } from "./human-input-surface";
|
|
28
43
|
import { MessageTimeline, type MessageTimelineProps } from "./message-timeline";
|
|
@@ -50,11 +65,14 @@ export type SessionConversationLabels = {
|
|
|
50
65
|
retry: string;
|
|
51
66
|
/** Action when the conversation could not load at all. */
|
|
52
67
|
tryAgain: string;
|
|
68
|
+
/** Shown in place of a Site preview when the session proxy does not serve Sites. */
|
|
69
|
+
sitePreviewUnavailable: string;
|
|
53
70
|
};
|
|
54
71
|
|
|
55
72
|
const DEFAULT_CONVERSATION_LABELS: SessionConversationLabels = {
|
|
56
73
|
retry: "Retry",
|
|
57
74
|
tryAgain: "Try again",
|
|
75
|
+
sitePreviewUnavailable: "Site preview unavailable",
|
|
58
76
|
};
|
|
59
77
|
|
|
60
78
|
export type SessionConversationProps = ClientOverride &
|
|
@@ -75,10 +93,17 @@ export type SessionConversationProps = ClientOverride &
|
|
|
75
93
|
* after `resolveLink`.
|
|
76
94
|
*/
|
|
77
95
|
onOpenArtifact?: ((target: OpenGeniViewerTarget) => void) | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Open another session the conversation points at: a sub-agent's chat from
|
|
98
|
+
* its worker card or a child update. `OpenGeniChat` opens it in place;
|
|
99
|
+
* without it those entries are not links.
|
|
100
|
+
*/
|
|
101
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
78
102
|
/**
|
|
79
103
|
* Inline previews for assistant `opengeni-site` / `opengeni-html` fences.
|
|
80
|
-
* Defaults to the OpenGeni preview
|
|
81
|
-
* `artifacts` option
|
|
104
|
+
* Defaults to the OpenGeni preview. Behind a session proxy without its
|
|
105
|
+
* `artifacts` option, Sites show as unavailable without a request; `false`
|
|
106
|
+
* shows the fence as code.
|
|
82
107
|
*/
|
|
83
108
|
renderInteractiveBlock?: MessageTimelineProps["renderInteractiveBlock"] | false;
|
|
84
109
|
/** Product-specific tool-call renderers; defaults to the built-in registry. */
|
|
@@ -95,6 +120,12 @@ export type SessionConversationProps = ClientOverride &
|
|
|
95
120
|
* appears only when the deployment's client config enables file uploads.
|
|
96
121
|
*/
|
|
97
122
|
attachments?: boolean | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Composer microphone (dictation into the draft). Defaults to true; it
|
|
125
|
+
* appears only when the client config reports voice input available
|
|
126
|
+
* (`createSessionProxyHandler({ voiceInput: false })` turns it off).
|
|
127
|
+
*/
|
|
128
|
+
voiceInput?: boolean | undefined;
|
|
98
129
|
/**
|
|
99
130
|
* Show the model/reasoning picker. End users of an embedded product rarely
|
|
100
131
|
* choose models, so it is hidden unless this is `true` or the client config
|
|
@@ -172,11 +203,13 @@ function Conversation({
|
|
|
172
203
|
renderMessageText,
|
|
173
204
|
resolveLink,
|
|
174
205
|
onOpenArtifact,
|
|
206
|
+
onOpenSession,
|
|
175
207
|
renderInteractiveBlock,
|
|
176
208
|
toolRegistry,
|
|
177
209
|
renderAllowanceExhausted,
|
|
178
210
|
allowanceExhaustedLabels,
|
|
179
211
|
attachments: attachmentsRequested = true,
|
|
212
|
+
voiceInput: voiceInputRequested = true,
|
|
180
213
|
modelPicker,
|
|
181
214
|
modelPickerProps,
|
|
182
215
|
userMessageDisclosureLabels,
|
|
@@ -238,6 +271,14 @@ function Conversation({
|
|
|
238
271
|
context.client,
|
|
239
272
|
context.workspaceId,
|
|
240
273
|
sessionId,
|
|
274
|
+
config.artifacts,
|
|
275
|
+
labelOverrides?.sitePreviewUnavailable ?? DEFAULT_CONVERSATION_LABELS.sitePreviewUnavailable,
|
|
276
|
+
);
|
|
277
|
+
const retainedLoaders = useRetainedLoaders(context.client, context.workspaceId, sessionId);
|
|
278
|
+
const transcription = useComposerTranscription(
|
|
279
|
+
context.client,
|
|
280
|
+
context.workspaceId,
|
|
281
|
+
voiceInputRequested && !importedArchive ? config.voiceInput : null,
|
|
241
282
|
);
|
|
242
283
|
const inheritedLinks = useOpenGeniLinkResolver();
|
|
243
284
|
const defaultLinks = useMemo(
|
|
@@ -336,6 +377,8 @@ function Conversation({
|
|
|
336
377
|
// Isolated and clipped: floating navigation stays inside the timeline.
|
|
337
378
|
className="isolate min-h-0 flex-1 overflow-hidden"
|
|
338
379
|
{...(toolRegistry ? { toolRegistry } : {})}
|
|
380
|
+
{...retainedLoaders}
|
|
381
|
+
{...(onOpenSession ? { onOpenSession } : {})}
|
|
339
382
|
events={feed.events}
|
|
340
383
|
items={conversationTimeline(feed.timeline, queue, composer)}
|
|
341
384
|
turnSummary={{ rolling: true }}
|
|
@@ -394,18 +437,24 @@ function Conversation({
|
|
|
394
437
|
autoFocus={false}
|
|
395
438
|
decisionButtons
|
|
396
439
|
/>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
440
|
+
<ConversationChrome
|
|
441
|
+
sessionId={sessionId}
|
|
442
|
+
client={context.client}
|
|
443
|
+
workspaceId={context.workspaceId}
|
|
444
|
+
events={feed.events}
|
|
445
|
+
chrome={
|
|
446
|
+
terminal
|
|
447
|
+
? { queue, sessionStatus: status, readOnly: true, onOpenSession }
|
|
448
|
+
: {
|
|
449
|
+
queue,
|
|
450
|
+
composer,
|
|
451
|
+
sessionStatus: status,
|
|
452
|
+
onOpenSession,
|
|
453
|
+
onComposerFocus: () =>
|
|
454
|
+
region.current?.querySelector<HTMLTextAreaElement>("textarea")?.focus(),
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/>
|
|
409
458
|
</div>
|
|
410
459
|
<div
|
|
411
460
|
className="relative z-20 mx-auto w-full max-w-3xl shrink-0"
|
|
@@ -415,6 +464,9 @@ function Conversation({
|
|
|
415
464
|
runControl="stop"
|
|
416
465
|
running={running}
|
|
417
466
|
{...composerProps}
|
|
467
|
+
{...(transcription && composerProps?.transcription === undefined
|
|
468
|
+
? { transcription }
|
|
469
|
+
: {})}
|
|
418
470
|
composer={composer}
|
|
419
471
|
attachments={uploadsEnabled ? files : undefined}
|
|
420
472
|
disabled={terminal || composerProps?.disabled}
|
|
@@ -456,34 +508,60 @@ function Conversation({
|
|
|
456
508
|
);
|
|
457
509
|
}
|
|
458
510
|
|
|
511
|
+
type ConversationChromeProps = {
|
|
512
|
+
sessionId: string;
|
|
513
|
+
client: unknown;
|
|
514
|
+
workspaceId: string;
|
|
515
|
+
events: UseGoalOptions["events"];
|
|
516
|
+
chrome: Omit<ComponentProps<typeof SessionChrome>, "goal">;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/** Session chrome with the goal pill and its Pause/Resume/Clear when the client can reach goals. */
|
|
520
|
+
function ConversationChrome(props: ConversationChromeProps) {
|
|
521
|
+
const goals = props.client as Partial<Record<"getGoal" | "updateGoal" | "deleteGoal", unknown>>;
|
|
522
|
+
const canReachGoals =
|
|
523
|
+
typeof goals.getGoal === "function" &&
|
|
524
|
+
typeof goals.updateGoal === "function" &&
|
|
525
|
+
typeof goals.deleteGoal === "function";
|
|
526
|
+
return canReachGoals ? <GoalChrome {...props} /> : <SessionChrome {...props.chrome} />;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function GoalChrome({ sessionId, client, workspaceId, events, chrome }: ConversationChromeProps) {
|
|
530
|
+
const goal = useGoal(sessionId, {
|
|
531
|
+
client: client as UseGoalOptions["client"],
|
|
532
|
+
workspaceId,
|
|
533
|
+
events,
|
|
534
|
+
});
|
|
535
|
+
return <SessionChrome {...chrome} goal={goal} />;
|
|
536
|
+
}
|
|
537
|
+
|
|
459
538
|
const LazyChatInteractiveBlock = lazy(() =>
|
|
460
539
|
import("./artifacts/chat-interactive-block").then((module) => ({
|
|
461
540
|
default: module.ChatInteractiveBlock,
|
|
462
541
|
})),
|
|
463
542
|
);
|
|
464
543
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
544
|
+
/** This client with the conversation's session scope header, when it can add headers. */
|
|
545
|
+
function sessionScoped<T>(client: T, sessionId: string): T {
|
|
546
|
+
const candidate = client as { withHeaders?: (headers: Readonly<Record<string, string>>) => T };
|
|
547
|
+
return typeof candidate.withHeaders === "function"
|
|
548
|
+
? candidate.withHeaders({ [SESSION_SCOPE_HEADER]: sessionId })
|
|
549
|
+
: client;
|
|
550
|
+
}
|
|
468
551
|
|
|
469
552
|
/** Inline Site/HTML preview reading through this conversation's session scope. */
|
|
470
553
|
function useDefaultInteractiveBlock(
|
|
471
554
|
client: unknown,
|
|
472
555
|
workspaceId: string,
|
|
473
556
|
sessionId: string,
|
|
557
|
+
sitesAvailable: boolean,
|
|
558
|
+
sitePreviewUnavailable: string,
|
|
474
559
|
): NonNullable<MessageTimelineProps["renderInteractiveBlock"]> {
|
|
475
560
|
// Scope lazily: only a rendered preview reads, and some clients (a Site's
|
|
476
561
|
// own client) cannot add headers.
|
|
477
562
|
const scoped = useMemo((): SiteSnapshotClient => {
|
|
478
563
|
let resolved: SiteSnapshotClient | null = null;
|
|
479
|
-
const get = () =>
|
|
480
|
-
const candidate = client as ScopableClient;
|
|
481
|
-
resolved ??=
|
|
482
|
-
typeof candidate.withHeaders === "function"
|
|
483
|
-
? candidate.withHeaders({ [SESSION_SCOPE_HEADER]: sessionId })
|
|
484
|
-
: candidate;
|
|
485
|
-
return resolved;
|
|
486
|
-
};
|
|
564
|
+
const get = () => (resolved ??= sessionScoped(client as SiteSnapshotClient, sessionId));
|
|
487
565
|
return {
|
|
488
566
|
getWorkspaceArtifact: (...args) => get().getWorkspaceArtifact(...args),
|
|
489
567
|
getWorkspaceArtifactHtml: (...args) => get().getWorkspaceArtifactHtml(...args),
|
|
@@ -495,54 +573,142 @@ function useDefaultInteractiveBlock(
|
|
|
495
573
|
};
|
|
496
574
|
}, [client, sessionId]);
|
|
497
575
|
return useCallback(
|
|
498
|
-
(block) =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
576
|
+
(block) =>
|
|
577
|
+
block.kind === "site" && !sitesAvailable ? (
|
|
578
|
+
// The proxy does not serve Sites: say so instead of offering a load
|
|
579
|
+
// that can only fail.
|
|
580
|
+
<div
|
|
581
|
+
className="rounded-og-md border border-og-border bg-og-surface-1 px-3 py-2 text-og-sm text-og-fg-muted"
|
|
582
|
+
data-og-site-preview-unavailable=""
|
|
583
|
+
>
|
|
584
|
+
{sitePreviewUnavailable}
|
|
585
|
+
</div>
|
|
586
|
+
) : (
|
|
587
|
+
<Suspense fallback={<span role="status">Loading preview…</span>}>
|
|
588
|
+
<LazyChatInteractiveBlock workspaceId={workspaceId} client={scoped} {...block} />
|
|
589
|
+
</Suspense>
|
|
590
|
+
),
|
|
591
|
+
[scoped, workspaceId, sitesAvailable, sitePreviewUnavailable],
|
|
504
592
|
);
|
|
505
593
|
}
|
|
506
594
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
595
|
+
type RetainedLoaderClient = Partial<
|
|
596
|
+
Pick<
|
|
597
|
+
OpenGeniClient,
|
|
598
|
+
| "createRetainedArtifactDownloadUrl"
|
|
599
|
+
| "downloadRetainedArtifact"
|
|
600
|
+
| "downloadRetainedScreenshot"
|
|
601
|
+
| "createVideoArtifactPlaybackSource"
|
|
602
|
+
>
|
|
603
|
+
>;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Generated images, published files, screenshots and generated video, read
|
|
607
|
+
* through this conversation's session scope (a session proxy only serves
|
|
608
|
+
* media that session produced). Clients without the SDK reads get none.
|
|
609
|
+
*/
|
|
610
|
+
function useRetainedLoaders(
|
|
611
|
+
client: unknown,
|
|
612
|
+
workspaceId: string,
|
|
613
|
+
sessionId: string,
|
|
614
|
+
): Pick<
|
|
615
|
+
MessageTimelineProps,
|
|
616
|
+
"loadRetainedArtifact" | "loadRetainedScreenshot" | "loadVideoArtifactPlayback"
|
|
617
|
+
> {
|
|
618
|
+
return useMemo(() => {
|
|
619
|
+
const base = client as RetainedLoaderClient;
|
|
620
|
+
// Scope lazily: only a rendered receipt reads.
|
|
621
|
+
let scoped: RetainedLoaderClient | null = null;
|
|
622
|
+
const get = () => (scoped ??= sessionScoped(base, sessionId));
|
|
623
|
+
return {
|
|
624
|
+
...(typeof base.downloadRetainedArtifact === "function" &&
|
|
625
|
+
typeof base.createRetainedArtifactDownloadUrl === "function"
|
|
626
|
+
? {
|
|
627
|
+
loadRetainedArtifact: (artifact, signal) =>
|
|
628
|
+
createWorkspaceRetainedArtifactLoader(
|
|
629
|
+
get() as Required<RetainedLoaderClient>,
|
|
630
|
+
workspaceId,
|
|
631
|
+
)(artifact, signal),
|
|
632
|
+
}
|
|
633
|
+
: {}),
|
|
634
|
+
...(typeof base.downloadRetainedScreenshot === "function"
|
|
635
|
+
? {
|
|
636
|
+
loadRetainedScreenshot: (artifact, signal) =>
|
|
637
|
+
createSessionRetainedScreenshotLoader(
|
|
638
|
+
get() as Required<RetainedLoaderClient>,
|
|
639
|
+
workspaceId,
|
|
640
|
+
sessionId,
|
|
641
|
+
)(artifact, signal),
|
|
642
|
+
}
|
|
643
|
+
: {}),
|
|
644
|
+
...(typeof base.createVideoArtifactPlaybackSource === "function"
|
|
645
|
+
? {
|
|
646
|
+
loadVideoArtifactPlayback: (artifactId, signal) =>
|
|
647
|
+
createWorkspaceRetainedVideoLoader(
|
|
648
|
+
get() as Required<RetainedLoaderClient>,
|
|
649
|
+
workspaceId,
|
|
650
|
+
)(artifactId, signal),
|
|
651
|
+
}
|
|
652
|
+
: {}),
|
|
653
|
+
} satisfies Pick<
|
|
654
|
+
MessageTimelineProps,
|
|
655
|
+
"loadRetainedArtifact" | "loadRetainedScreenshot" | "loadVideoArtifactPlayback"
|
|
656
|
+
>;
|
|
657
|
+
}, [client, workspaceId, sessionId]);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The composer microphone: shown when the deployment can transcribe, enabled
|
|
662
|
+
* once the workspace's voice-input setting allows it.
|
|
663
|
+
*/
|
|
664
|
+
export function useComposerTranscription(
|
|
665
|
+
client: unknown,
|
|
666
|
+
workspaceId: string,
|
|
667
|
+
capability: ClientVoiceInputConfig | null,
|
|
668
|
+
): ComposerTranscriptionControlProps | null {
|
|
669
|
+
const [workspaceEnabled, setWorkspaceEnabled] = useState<{
|
|
670
|
+
key: string;
|
|
671
|
+
enabled: boolean;
|
|
672
|
+
} | null>(null);
|
|
673
|
+
const reader = client as Partial<Pick<OpenGeniClient, "getWorkspace" | "transcribeAudio">>;
|
|
674
|
+
const supported = capability !== null && typeof reader.transcribeAudio === "function";
|
|
527
675
|
useEffect(() => {
|
|
676
|
+
if (!supported) return;
|
|
528
677
|
let live = true;
|
|
529
|
-
|
|
530
|
-
|
|
678
|
+
const key = workspaceId;
|
|
679
|
+
if (typeof reader.getWorkspace !== "function") {
|
|
680
|
+
setWorkspaceEnabled({ key, enabled: true });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
reader.getWorkspace(workspaceId).then(
|
|
684
|
+
(workspace) => {
|
|
531
685
|
if (live) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
modelSelection: config.modelSelection === true,
|
|
536
|
-
sandboxFiles: config.sandboxFiles !== false,
|
|
537
|
-
defaultReasoningEffort: config.defaultReasoningEffort ?? null,
|
|
686
|
+
setWorkspaceEnabled({
|
|
687
|
+
key,
|
|
688
|
+
enabled: resolveWorkspaceVoiceInputEnabled(workspace.settings) ?? true,
|
|
538
689
|
});
|
|
539
690
|
}
|
|
540
691
|
},
|
|
541
|
-
|
|
692
|
+
// The transcription request still enforces the setting.
|
|
693
|
+
() => live && setWorkspaceEnabled({ key, enabled: true }),
|
|
542
694
|
);
|
|
543
695
|
return () => {
|
|
544
696
|
live = false;
|
|
545
697
|
};
|
|
546
|
-
}, [
|
|
547
|
-
return
|
|
698
|
+
}, [reader, workspaceId, supported]);
|
|
699
|
+
return useMemo(
|
|
700
|
+
() =>
|
|
701
|
+
supported
|
|
702
|
+
? {
|
|
703
|
+
client: reader as Pick<OpenGeniClient, "transcribeAudio">,
|
|
704
|
+
workspaceId,
|
|
705
|
+
capability,
|
|
706
|
+
workspaceEnabled:
|
|
707
|
+
workspaceEnabled?.key === workspaceId ? workspaceEnabled.enabled : false,
|
|
708
|
+
}
|
|
709
|
+
: null,
|
|
710
|
+
[supported, reader, workspaceId, capability, workspaceEnabled],
|
|
711
|
+
);
|
|
548
712
|
}
|
|
713
|
+
|
|
714
|
+
export { useClientConfigFlags } from "../hooks/use-client-config-flags";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deriveSessionDisplayTitle, type OpenGeniClient, type Session } from "@opengeni/sdk";
|
|
2
2
|
import { ArchiveIcon, PencilIcon, PlusIcon } from "lucide-react";
|
|
3
3
|
import { useEffect, useRef, useState, type FormEvent } from "react";
|
|
4
|
+
import { useClientConfigFlags } from "../hooks/use-client-config-flags";
|
|
4
5
|
import { useWorkspaceSessions } from "../hooks/use-workspace-sessions";
|
|
5
6
|
import { cn } from "../lib/cn";
|
|
6
7
|
import { formatRelativeTime } from "../lib/format";
|
|
@@ -38,7 +39,10 @@ export type SessionListProps = ClientOverride & {
|
|
|
38
39
|
onNewChat?: (() => void) | undefined;
|
|
39
40
|
/** Allow inline rename (`updateSession`). Defaults to true. */
|
|
40
41
|
rename?: boolean | undefined;
|
|
41
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Allow archiving (`updateSessionArchive`). Defaults to true; hidden when the
|
|
44
|
+
* session proxy reports archive disabled (`archive: false`).
|
|
45
|
+
*/
|
|
42
46
|
archive?: boolean | undefined;
|
|
43
47
|
/** Called after the selected chat was archived, so the host can move on. */
|
|
44
48
|
onArchived?: ((sessionId: string) => void) | undefined;
|
|
@@ -94,7 +98,10 @@ export function SessionList({
|
|
|
94
98
|
const [error, setError] = useState<{ cause: unknown } | null>(null);
|
|
95
99
|
const formatError = useErrorMessage();
|
|
96
100
|
const archiveClient = context.client as unknown as ArchiveClient;
|
|
97
|
-
|
|
101
|
+
// A session proxy with `archive: false` reports it; hide what would fail.
|
|
102
|
+
const config = useClientConfigFlags(context.client as never);
|
|
103
|
+
const canArchive =
|
|
104
|
+
archive && config.archive && typeof archiveClient.updateSessionArchive === "function";
|
|
98
105
|
|
|
99
106
|
const run = async (id: string, action: () => Promise<unknown>) => {
|
|
100
107
|
setBusy(id);
|