@opengeni/react 1.2.1-canary.37222965366001 → 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 +52 -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-4OTO7KBK.js → chunk-LJ2UTJJE.js} +2737 -1580
- 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-X34BSY2R.js → chunk-WZMYFJD3.js} +242 -63
- package/dist/chunk-WZMYFJD3.js.map +1 -0
- package/dist/components/approval-surface.d.ts +11 -13
- package/dist/components/composer-transcription-control.d.ts +3 -0
- package/dist/components/open-geni-chat.d.ts +26 -7
- package/dist/components/session-conversation.d.ts +26 -3
- package/dist/components/session-list.d.ts +4 -1
- package/dist/components/session-proxy-scope.d.ts +16 -2
- package/dist/components/tool-action-review.d.ts +47 -0
- package/dist/components/tool-review-history.d.ts +25 -0
- 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 +7 -4
- package/dist/index.js +27 -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.d.ts +4 -1
- package/dist/session-ui.js +12 -6
- package/dist/session-ui.js.map +1 -1
- 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 +368 -239
- 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 +245 -72
- package/src/components/session-conversation.tsx +243 -58
- package/src/components/session-list.tsx +9 -2
- package/src/components/session-proxy-scope.tsx +47 -7
- package/src/components/tool-action-review.tsx +649 -0
- package/src/components/tool-review-history.tsx +88 -0
- 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 +18 -1
- package/src/realtime/realtime-control.tsx +28 -1
- package/src/session-capability-policy.ts +119 -6
- package/src/session-ui.ts +9 -1
- 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 +148 -8
- package/styles/compiled.css +79 -15
- package/styles/tokens.css +1 -1
- package/dist/chunk-4OTO7KBK.js.map +0 -1
- package/dist/chunk-742CZ3L3.js.map +0 -1
- package/dist/chunk-CY7WK5FD.js.map +0 -1
- package/dist/chunk-TMYGL6X5.js.map +0 -1
- package/dist/chunk-X34BSY2R.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
|
+
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
|
|
@@ -134,11 +165,22 @@ export type SessionConversationProps = ClientOverride &
|
|
|
134
165
|
* (including Site clients), one shared event feed, and authoritative queue state.
|
|
135
166
|
* `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />` needs no
|
|
136
167
|
* provider: it talks to your session proxy and its resolved workspace. */
|
|
137
|
-
export function SessionConversation({
|
|
168
|
+
export function SessionConversation({
|
|
169
|
+
baseUrl,
|
|
170
|
+
headers,
|
|
171
|
+
fetch,
|
|
172
|
+
...props
|
|
173
|
+
}: SessionConversationProps) {
|
|
138
174
|
if (baseUrl === undefined) return <RetryingConversation {...props} />;
|
|
139
|
-
const { client
|
|
175
|
+
const { client, workspaceId, ...rest } = props;
|
|
140
176
|
return (
|
|
141
|
-
<SessionProxyScope
|
|
177
|
+
<SessionProxyScope
|
|
178
|
+
baseUrl={baseUrl}
|
|
179
|
+
workspaceId={workspaceId}
|
|
180
|
+
client={client}
|
|
181
|
+
headers={headers}
|
|
182
|
+
fetch={fetch}
|
|
183
|
+
>
|
|
142
184
|
<RetryingConversation {...rest} />
|
|
143
185
|
</SessionProxyScope>
|
|
144
186
|
);
|
|
@@ -161,11 +203,13 @@ function Conversation({
|
|
|
161
203
|
renderMessageText,
|
|
162
204
|
resolveLink,
|
|
163
205
|
onOpenArtifact,
|
|
206
|
+
onOpenSession,
|
|
164
207
|
renderInteractiveBlock,
|
|
165
208
|
toolRegistry,
|
|
166
209
|
renderAllowanceExhausted,
|
|
167
210
|
allowanceExhaustedLabels,
|
|
168
211
|
attachments: attachmentsRequested = true,
|
|
212
|
+
voiceInput: voiceInputRequested = true,
|
|
169
213
|
modelPicker,
|
|
170
214
|
modelPickerProps,
|
|
171
215
|
userMessageDisclosureLabels,
|
|
@@ -227,6 +271,14 @@ function Conversation({
|
|
|
227
271
|
context.client,
|
|
228
272
|
context.workspaceId,
|
|
229
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,
|
|
230
282
|
);
|
|
231
283
|
const inheritedLinks = useOpenGeniLinkResolver();
|
|
232
284
|
const defaultLinks = useMemo(
|
|
@@ -325,6 +377,8 @@ function Conversation({
|
|
|
325
377
|
// Isolated and clipped: floating navigation stays inside the timeline.
|
|
326
378
|
className="isolate min-h-0 flex-1 overflow-hidden"
|
|
327
379
|
{...(toolRegistry ? { toolRegistry } : {})}
|
|
380
|
+
{...retainedLoaders}
|
|
381
|
+
{...(onOpenSession ? { onOpenSession } : {})}
|
|
328
382
|
events={feed.events}
|
|
329
383
|
items={conversationTimeline(feed.timeline, queue, composer)}
|
|
330
384
|
turnSummary={{ rolling: true }}
|
|
@@ -383,18 +437,24 @@ function Conversation({
|
|
|
383
437
|
autoFocus={false}
|
|
384
438
|
decisionButtons
|
|
385
439
|
/>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
/>
|
|
398
458
|
</div>
|
|
399
459
|
<div
|
|
400
460
|
className="relative z-20 mx-auto w-full max-w-3xl shrink-0"
|
|
@@ -404,6 +464,9 @@ function Conversation({
|
|
|
404
464
|
runControl="stop"
|
|
405
465
|
running={running}
|
|
406
466
|
{...composerProps}
|
|
467
|
+
{...(transcription && composerProps?.transcription === undefined
|
|
468
|
+
? { transcription }
|
|
469
|
+
: {})}
|
|
407
470
|
composer={composer}
|
|
408
471
|
attachments={uploadsEnabled ? files : undefined}
|
|
409
472
|
disabled={terminal || composerProps?.disabled}
|
|
@@ -445,34 +508,60 @@ function Conversation({
|
|
|
445
508
|
);
|
|
446
509
|
}
|
|
447
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
|
+
|
|
448
538
|
const LazyChatInteractiveBlock = lazy(() =>
|
|
449
539
|
import("./artifacts/chat-interactive-block").then((module) => ({
|
|
450
540
|
default: module.ChatInteractiveBlock,
|
|
451
541
|
})),
|
|
452
542
|
);
|
|
453
543
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
};
|
|
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
|
+
}
|
|
457
551
|
|
|
458
552
|
/** Inline Site/HTML preview reading through this conversation's session scope. */
|
|
459
553
|
function useDefaultInteractiveBlock(
|
|
460
554
|
client: unknown,
|
|
461
555
|
workspaceId: string,
|
|
462
556
|
sessionId: string,
|
|
557
|
+
sitesAvailable: boolean,
|
|
558
|
+
sitePreviewUnavailable: string,
|
|
463
559
|
): NonNullable<MessageTimelineProps["renderInteractiveBlock"]> {
|
|
464
560
|
// Scope lazily: only a rendered preview reads, and some clients (a Site's
|
|
465
561
|
// own client) cannot add headers.
|
|
466
562
|
const scoped = useMemo((): SiteSnapshotClient => {
|
|
467
563
|
let resolved: SiteSnapshotClient | null = null;
|
|
468
|
-
const get = () =>
|
|
469
|
-
const candidate = client as ScopableClient;
|
|
470
|
-
resolved ??=
|
|
471
|
-
typeof candidate.withHeaders === "function"
|
|
472
|
-
? candidate.withHeaders({ [SESSION_SCOPE_HEADER]: sessionId })
|
|
473
|
-
: candidate;
|
|
474
|
-
return resolved;
|
|
475
|
-
};
|
|
564
|
+
const get = () => (resolved ??= sessionScoped(client as SiteSnapshotClient, sessionId));
|
|
476
565
|
return {
|
|
477
566
|
getWorkspaceArtifact: (...args) => get().getWorkspaceArtifact(...args),
|
|
478
567
|
getWorkspaceArtifactHtml: (...args) => get().getWorkspaceArtifactHtml(...args),
|
|
@@ -484,46 +573,142 @@ function useDefaultInteractiveBlock(
|
|
|
484
573
|
};
|
|
485
574
|
}, [client, sessionId]);
|
|
486
575
|
return useCallback(
|
|
487
|
-
(block) =>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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],
|
|
493
592
|
);
|
|
494
593
|
}
|
|
495
594
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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";
|
|
509
675
|
useEffect(() => {
|
|
676
|
+
if (!supported) return;
|
|
510
677
|
let live = true;
|
|
511
|
-
|
|
512
|
-
|
|
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) => {
|
|
513
685
|
if (live) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
modelSelection: config.modelSelection === true,
|
|
518
|
-
sandboxFiles: config.sandboxFiles !== false,
|
|
686
|
+
setWorkspaceEnabled({
|
|
687
|
+
key,
|
|
688
|
+
enabled: resolveWorkspaceVoiceInputEnabled(workspace.settings) ?? true,
|
|
519
689
|
});
|
|
520
690
|
}
|
|
521
691
|
},
|
|
522
|
-
|
|
692
|
+
// The transcription request still enforces the setting.
|
|
693
|
+
() => live && setWorkspaceEnabled({ key, enabled: true }),
|
|
523
694
|
);
|
|
524
695
|
return () => {
|
|
525
696
|
live = false;
|
|
526
697
|
};
|
|
527
|
-
}, [
|
|
528
|
-
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
|
+
);
|
|
529
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);
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import { OpenGeniBrowserClient } from "@opengeni/sdk/browser";
|
|
2
|
-
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
1
|
+
import { OpenGeniBrowserClient, type FetchLike } from "@opengeni/sdk/browser";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
3
|
+
import type { SessionClientLike } from "../client";
|
|
3
4
|
import { OpenGeniProvider } from "../provider";
|
|
4
5
|
|
|
6
|
+
/** Request headers for every proxy call: static, or computed per request. */
|
|
7
|
+
export type SessionProxyHeaders = Record<string, string> | (() => Record<string, string>);
|
|
8
|
+
|
|
5
9
|
export type SessionProxyBaseUrl = {
|
|
6
10
|
/**
|
|
7
11
|
* Mount path of your `createSessionProxyHandler` (for example
|
|
8
12
|
* `"/api/opengeni"`). The component then needs no `OpenGeniProvider`,
|
|
9
13
|
* client, or workspace id: it creates the browser client for that path and
|
|
10
14
|
* uses the workspace the proxy resolved for the signed-in user. An explicit
|
|
11
|
-
* `workspaceId` skips that lookup.
|
|
15
|
+
* `workspaceId` skips that lookup. A `client` you pass is used instead of
|
|
16
|
+
* the created one (point it at the same proxy).
|
|
12
17
|
*/
|
|
13
18
|
baseUrl?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* `baseUrl` mode: headers added to every proxy request, for hosts that
|
|
21
|
+
* authenticate with a bearer token instead of cookies. A function is called
|
|
22
|
+
* per request, so it can return the current token.
|
|
23
|
+
*/
|
|
24
|
+
headers?: SessionProxyHeaders | undefined;
|
|
25
|
+
/** `baseUrl` mode: custom fetch for every proxy request (async auth, retries). */
|
|
26
|
+
fetch?: FetchLike | undefined;
|
|
14
27
|
};
|
|
15
28
|
|
|
16
29
|
type ScopeState =
|
|
@@ -26,18 +39,45 @@ type ScopeState =
|
|
|
26
39
|
export function SessionProxyScope({
|
|
27
40
|
baseUrl,
|
|
28
41
|
workspaceId,
|
|
42
|
+
client: suppliedClient,
|
|
43
|
+
headers,
|
|
44
|
+
fetch,
|
|
29
45
|
children,
|
|
30
|
-
}: {
|
|
46
|
+
}: Omit<SessionProxyBaseUrl, "baseUrl"> & {
|
|
31
47
|
baseUrl: string;
|
|
32
48
|
workspaceId?: string | undefined;
|
|
49
|
+
client?: SessionClientLike | undefined;
|
|
33
50
|
children: ReactNode;
|
|
34
51
|
}) {
|
|
52
|
+
if (suppliedClient && (headers !== undefined || fetch !== undefined)) {
|
|
53
|
+
throw new TypeError(
|
|
54
|
+
"@opengeni/react: pass headers/fetch either to your own client or as props, not both.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Latest values without recreating the client (inline props change identity).
|
|
58
|
+
const headersRef = useRef(headers);
|
|
59
|
+
headersRef.current = headers;
|
|
60
|
+
const fetchRef = useRef(fetch);
|
|
61
|
+
fetchRef.current = fetch;
|
|
62
|
+
const hasFetch = fetch !== undefined;
|
|
35
63
|
// Version skew between this bundle and the server SDK is tolerated, as for
|
|
36
64
|
// any embedded host: the proxy reports its own contract revision.
|
|
37
|
-
const
|
|
38
|
-
() =>
|
|
39
|
-
|
|
65
|
+
const ownClient = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
new OpenGeniBrowserClient({
|
|
68
|
+
baseUrl,
|
|
69
|
+
apiContract: "compatible",
|
|
70
|
+
headers: () => {
|
|
71
|
+
const current = headersRef.current;
|
|
72
|
+
return typeof current === "function" ? current() : (current ?? {});
|
|
73
|
+
},
|
|
74
|
+
...(hasFetch
|
|
75
|
+
? { fetch: (input, init) => (fetchRef.current ?? globalThis.fetch)(input, init) }
|
|
76
|
+
: {}),
|
|
77
|
+
}),
|
|
78
|
+
[baseUrl, hasFetch],
|
|
40
79
|
);
|
|
80
|
+
const client: SessionClientLike = suppliedClient ?? ownClient;
|
|
41
81
|
const [state, setState] = useState<ScopeState>(
|
|
42
82
|
workspaceId ? { status: "ready", workspaceId } : { status: "loading" },
|
|
43
83
|
);
|