@opengeni/react 1.2.1-canary.37222965366001 → 1.3.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 +31 -0
- package/dist/{chunk-4OTO7KBK.js → chunk-PGSX7UCP.js} +2062 -1489
- package/dist/chunk-PGSX7UCP.js.map +1 -0
- package/dist/{chunk-X34BSY2R.js → chunk-WWEUO2PL.js} +60 -60
- package/dist/chunk-WWEUO2PL.js.map +1 -0
- package/dist/components/approval-surface.d.ts +11 -13
- package/dist/components/open-geni-chat.d.ts +21 -6
- package/dist/components/session-conversation.d.ts +18 -1
- package/dist/components/session-proxy-scope.d.ts +16 -2
- package/dist/components/tool-action-review.d.ts +20 -0
- package/dist/components/tool-review-history.d.ts +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/session-ui.d.ts +4 -1
- package/dist/session-ui.js +8 -2
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +1 -1
- package/package.json +3 -3
- package/src/components/approval-surface.tsx +263 -245
- package/src/components/open-geni-chat.tsx +197 -64
- package/src/components/session-conversation.tsx +26 -7
- package/src/components/session-proxy-scope.tsx +47 -7
- package/src/components/tool-action-review.tsx +386 -0
- package/src/components/tool-review-history.tsx +113 -0
- package/src/index.ts +9 -1
- package/src/session-ui.ts +9 -1
- package/src/timeline/tool-renderers.tsx +7 -0
- package/styles/compiled.css +33 -13
- package/dist/chunk-4OTO7KBK.js.map +0 -1
- package/dist/chunk-X34BSY2R.js.map +0 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { OpenGeniClient } from "@opengeni/sdk";
|
|
2
|
-
import {
|
|
1
|
+
import type { FileResourceRef, LatencyMode, OpenGeniClient, ReasoningEffort } from "@opengeni/sdk";
|
|
2
|
+
import { MenuIcon, XIcon } from "lucide-react";
|
|
3
3
|
import { useCallback, useRef, useState, type CSSProperties, type FormEvent } from "react";
|
|
4
|
+
import { useWorkspaceModelCatalog } from "../hooks/use-available-models";
|
|
5
|
+
import { FILE_ONLY_MESSAGE_TEXT, type ComposerState } from "../hooks/use-composer";
|
|
6
|
+
import { useFileAttachments } from "../hooks/use-file-attachments";
|
|
4
7
|
import { cn } from "../lib/cn";
|
|
5
8
|
import { useErrorMessage } from "../lib/error-message";
|
|
6
9
|
import {
|
|
@@ -9,7 +12,13 @@ import {
|
|
|
9
12
|
type HostThemePreference,
|
|
10
13
|
} from "../lib/host-theme";
|
|
11
14
|
import { useOpenGeni, type ClientOverride } from "../session-context";
|
|
12
|
-
import {
|
|
15
|
+
import { ChatComposer } from "./chat-composer";
|
|
16
|
+
import { ModelPolicyPicker } from "./model-policy-picker";
|
|
17
|
+
import {
|
|
18
|
+
SessionConversation,
|
|
19
|
+
useClientConfigFlags,
|
|
20
|
+
type SessionConversationProps,
|
|
21
|
+
} from "./session-conversation";
|
|
13
22
|
import { SessionList, type SessionListLabels, type SessionListProps } from "./session-list";
|
|
14
23
|
import { SessionProxyScope, type SessionProxyBaseUrl } from "./session-proxy-scope";
|
|
15
24
|
|
|
@@ -32,6 +41,16 @@ const DEFAULT_LABELS: Omit<OpenGeniChatLabels, keyof SessionListLabels> = {
|
|
|
32
41
|
newChatUnavailable: "New chats are not enabled for this product.",
|
|
33
42
|
};
|
|
34
43
|
|
|
44
|
+
/** What the first message carries besides its text. */
|
|
45
|
+
export type OpenGeniChatCreateOptions = {
|
|
46
|
+
/** Files attached in the new-chat composer. */
|
|
47
|
+
resources?: FileResourceRef[] | undefined;
|
|
48
|
+
/** Present only when the user changed it in the model picker. */
|
|
49
|
+
model?: string | undefined;
|
|
50
|
+
reasoningEffort?: ReasoningEffort | undefined;
|
|
51
|
+
latencyMode?: LatencyMode | undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
35
54
|
export type OpenGeniChatProps = ClientOverride &
|
|
36
55
|
SessionProxyBaseUrl & {
|
|
37
56
|
/** Controlled selection. `null` shows the new-chat composer. */
|
|
@@ -41,18 +60,27 @@ export type OpenGeniChatProps = ClientOverride &
|
|
|
41
60
|
onSessionChange?: ((sessionId: string | null) => void) | undefined;
|
|
42
61
|
/**
|
|
43
62
|
* Create a chat from its first message. Defaults to `client.createSession`
|
|
44
|
-
* with `{ initialMessage, idempotencyKey }
|
|
45
|
-
*
|
|
63
|
+
* with `{ initialMessage, idempotencyKey }` plus the attached files and any
|
|
64
|
+
* model choice, which `createSessionProxyHandler` accepts when the server
|
|
65
|
+
* supplies a `createSession` hook. Return the new id.
|
|
46
66
|
*/
|
|
47
67
|
createSession?:
|
|
48
|
-
| ((
|
|
68
|
+
| ((
|
|
69
|
+
initialMessage: string,
|
|
70
|
+
idempotencyKey: string,
|
|
71
|
+
options: OpenGeniChatCreateOptions,
|
|
72
|
+
) => Promise<string>)
|
|
49
73
|
| undefined;
|
|
50
74
|
/** Hide the "New chat" entry point. */
|
|
51
75
|
newChat?: boolean | undefined;
|
|
52
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Forwarded to the conversation (message rendering, tool renderers,
|
|
78
|
+
* composer...). `attachments`, `modelPicker`, `modelPickerProps`, and
|
|
79
|
+
* `composerProps` also apply to the new-chat composer.
|
|
80
|
+
*/
|
|
53
81
|
conversationProps?: Omit<
|
|
54
82
|
SessionConversationProps,
|
|
55
|
-
"sessionId" | "client" | "workspaceId" | "baseUrl"
|
|
83
|
+
"sessionId" | "client" | "workspaceId" | "baseUrl" | "headers" | "fetch"
|
|
56
84
|
>;
|
|
57
85
|
/** Forwarded to the list (rename/archive toggles, page size). */
|
|
58
86
|
listProps?: Pick<SessionListProps, "rename" | "archive" | "pageSize"> | undefined;
|
|
@@ -83,11 +111,17 @@ type CreateClient = Partial<Pick<OpenGeniClient, "createSession">>;
|
|
|
83
111
|
* `<OpenGeniChat baseUrl="/api/opengeni" />` needs no provider: it talks to
|
|
84
112
|
* your session proxy and uses the workspace the proxy resolves.
|
|
85
113
|
*/
|
|
86
|
-
export function OpenGeniChat({ baseUrl, ...props }: OpenGeniChatProps) {
|
|
114
|
+
export function OpenGeniChat({ baseUrl, headers, fetch, ...props }: OpenGeniChatProps) {
|
|
87
115
|
if (baseUrl === undefined) return <Chat {...props} />;
|
|
88
|
-
const { client
|
|
116
|
+
const { client, workspaceId, ...rest } = props;
|
|
89
117
|
return (
|
|
90
|
-
<SessionProxyScope
|
|
118
|
+
<SessionProxyScope
|
|
119
|
+
baseUrl={baseUrl}
|
|
120
|
+
workspaceId={workspaceId}
|
|
121
|
+
client={client}
|
|
122
|
+
headers={headers}
|
|
123
|
+
fetch={fetch}
|
|
124
|
+
>
|
|
91
125
|
<Chat {...rest} />
|
|
92
126
|
</SessionProxyScope>
|
|
93
127
|
);
|
|
@@ -108,7 +142,7 @@ function Chat({
|
|
|
108
142
|
height = "100%",
|
|
109
143
|
theme,
|
|
110
144
|
surface,
|
|
111
|
-
}: Omit<OpenGeniChatProps, "baseUrl">) {
|
|
145
|
+
}: Omit<OpenGeniChatProps, "baseUrl" | "headers" | "fetch">) {
|
|
112
146
|
const labels = { ...DEFAULT_LABELS, ...labelOverrides } as OpenGeniChatLabels;
|
|
113
147
|
const scope = { client, workspaceId };
|
|
114
148
|
const context = useOpenGeni(scope);
|
|
@@ -129,13 +163,18 @@ function Chat({
|
|
|
129
163
|
);
|
|
130
164
|
|
|
131
165
|
const create = useCallback(
|
|
132
|
-
async (
|
|
133
|
-
|
|
166
|
+
async (
|
|
167
|
+
initialMessage: string,
|
|
168
|
+
idempotencyKey: string,
|
|
169
|
+
options: OpenGeniChatCreateOptions,
|
|
170
|
+
): Promise<string> => {
|
|
171
|
+
if (createSession) return await createSession(initialMessage, idempotencyKey, options);
|
|
134
172
|
const creator = (context.client as unknown as CreateClient).createSession;
|
|
135
173
|
if (typeof creator !== "function") throw new Error(labels.newChatUnavailable);
|
|
136
174
|
const created = await creator.call(context.client, context.workspaceId, {
|
|
137
175
|
initialMessage,
|
|
138
176
|
idempotencyKey,
|
|
177
|
+
...options,
|
|
139
178
|
} as Parameters<NonNullable<CreateClient["createSession"]>>[1]);
|
|
140
179
|
return created.id;
|
|
141
180
|
},
|
|
@@ -223,7 +262,10 @@ function Chat({
|
|
|
223
262
|
/>
|
|
224
263
|
) : (
|
|
225
264
|
<NewChat
|
|
265
|
+
scope={scope}
|
|
226
266
|
labels={labels}
|
|
267
|
+
placeholderOverride={labelOverrides?.newChatPlaceholder}
|
|
268
|
+
conversationProps={conversationProps}
|
|
227
269
|
create={create}
|
|
228
270
|
onCreated={(id) => {
|
|
229
271
|
setListRevision((revision) => revision + 1);
|
|
@@ -237,43 +279,137 @@ function Chat({
|
|
|
237
279
|
);
|
|
238
280
|
}
|
|
239
281
|
|
|
282
|
+
const NOOP_ASYNC = async () => {};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The first message of a new chat, in the same composer as follow-ups: file
|
|
286
|
+
* attachments, the model picker when offered, and the host's `composerProps`
|
|
287
|
+
* (custom controls, voice input, copy).
|
|
288
|
+
*/
|
|
240
289
|
function NewChat({
|
|
290
|
+
scope,
|
|
241
291
|
labels,
|
|
292
|
+
placeholderOverride,
|
|
293
|
+
conversationProps,
|
|
242
294
|
create,
|
|
243
295
|
onCreated,
|
|
244
296
|
}: {
|
|
297
|
+
scope: ClientOverride;
|
|
245
298
|
labels: OpenGeniChatLabels;
|
|
246
|
-
|
|
299
|
+
placeholderOverride: string | undefined;
|
|
300
|
+
conversationProps: OpenGeniChatProps["conversationProps"];
|
|
301
|
+
create: (
|
|
302
|
+
initialMessage: string,
|
|
303
|
+
idempotencyKey: string,
|
|
304
|
+
options: OpenGeniChatCreateOptions,
|
|
305
|
+
) => Promise<string>;
|
|
247
306
|
onCreated: (sessionId: string) => void;
|
|
248
307
|
}) {
|
|
308
|
+
const context = useOpenGeni(scope);
|
|
309
|
+
const config = useClientConfigFlags(context.client);
|
|
310
|
+
const composerProps = conversationProps?.composerProps;
|
|
311
|
+
const modelPickerProps = conversationProps?.modelPickerProps;
|
|
312
|
+
const showModelPicker = conversationProps?.modelPicker ?? config.modelSelection;
|
|
313
|
+
const catalog = useWorkspaceModelCatalog({
|
|
314
|
+
client: context.client,
|
|
315
|
+
workspaceId: context.workspaceId,
|
|
316
|
+
enabled: showModelPicker,
|
|
317
|
+
});
|
|
318
|
+
const files = useFileAttachments(scope);
|
|
319
|
+
const uploadsEnabled = (conversationProps?.attachments ?? true) && config.uploads;
|
|
249
320
|
const [text, setText] = useState("");
|
|
250
321
|
const [sending, setSending] = useState(false);
|
|
251
322
|
const [error, setError] = useState<{ cause: unknown } | null>(null);
|
|
323
|
+
// Only what the user changed is sent; everything else stays server-owned.
|
|
324
|
+
const [choice, setChoice] = useState<
|
|
325
|
+
Pick<OpenGeniChatCreateOptions, "model" | "reasoningEffort" | "latencyMode">
|
|
326
|
+
>({});
|
|
252
327
|
const formatError = useErrorMessage();
|
|
253
|
-
// One key per
|
|
254
|
-
|
|
328
|
+
// One key per attempted payload, so a retried send returns the same chat
|
|
329
|
+
// and an edited retry is a new request rather than an idempotency conflict.
|
|
330
|
+
const attempt = useRef<{ fingerprint: string; key: string } | null>(null);
|
|
331
|
+
const resources = uploadsEnabled ? files.readyResources : [];
|
|
332
|
+
const pendingFiles = uploadsEnabled && files.hasUnresolved;
|
|
255
333
|
|
|
256
|
-
const submit = async (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
334
|
+
const submit = async (): Promise<boolean> => {
|
|
335
|
+
const initialMessage = text.trim() || (resources.length > 0 ? FILE_ONLY_MESSAGE_TEXT : "");
|
|
336
|
+
if (!initialMessage || sending || pendingFiles) return false;
|
|
337
|
+
const options: OpenGeniChatCreateOptions = {
|
|
338
|
+
...(resources.length > 0 ? { resources } : {}),
|
|
339
|
+
...(showModelPicker ? choice : {}),
|
|
340
|
+
};
|
|
341
|
+
const fingerprint = JSON.stringify([initialMessage, options]);
|
|
342
|
+
if (attempt.current?.fingerprint !== fingerprint) {
|
|
343
|
+
attempt.current = { fingerprint, key: crypto.randomUUID() };
|
|
344
|
+
}
|
|
260
345
|
setSending(true);
|
|
261
346
|
setError(null);
|
|
262
347
|
try {
|
|
263
|
-
const id = await create(initialMessage,
|
|
264
|
-
|
|
348
|
+
const id = await create(initialMessage, attempt.current.key, options);
|
|
349
|
+
attempt.current = null;
|
|
265
350
|
setText("");
|
|
351
|
+
files.removeReadyFiles(resources.map((resource) => resource.fileId));
|
|
266
352
|
onCreated(id);
|
|
353
|
+
return true;
|
|
267
354
|
} catch (cause) {
|
|
268
355
|
setError({ cause });
|
|
356
|
+
return false;
|
|
269
357
|
} finally {
|
|
270
358
|
setSending(false);
|
|
271
359
|
}
|
|
272
360
|
};
|
|
273
361
|
|
|
362
|
+
const defaultModel = catalog.defaultModel;
|
|
363
|
+
const policy =
|
|
364
|
+
showModelPicker && (choice.model ?? defaultModel)
|
|
365
|
+
? {
|
|
366
|
+
model: (choice.model ?? defaultModel)!,
|
|
367
|
+
reasoningEffort:
|
|
368
|
+
choice.reasoningEffort ?? config.defaultReasoningEffort ?? ("medium" as const),
|
|
369
|
+
latencyMode: choice.latencyMode ?? ("standard" as const),
|
|
370
|
+
}
|
|
371
|
+
: null;
|
|
372
|
+
|
|
373
|
+
const composer: ComposerState = {
|
|
374
|
+
value: text,
|
|
375
|
+
setValue: setText,
|
|
376
|
+
hasDraftContent: () => text.length > 0 || files.attachments.length > 0,
|
|
377
|
+
send: submit,
|
|
378
|
+
steer: submit,
|
|
379
|
+
sending,
|
|
380
|
+
canSend: (text.trim().length > 0 || resources.length > 0) && !sending && !pendingFiles,
|
|
381
|
+
pause: NOOP_ASYNC,
|
|
382
|
+
pausing: false,
|
|
383
|
+
resume: NOOP_ASYNC,
|
|
384
|
+
resumeScope: NOOP_ASYNC,
|
|
385
|
+
resuming: false,
|
|
386
|
+
draft: null,
|
|
387
|
+
draftRevision: 0,
|
|
388
|
+
draftLoading: false,
|
|
389
|
+
draftSaving: false,
|
|
390
|
+
draftConflict: null,
|
|
391
|
+
policy,
|
|
392
|
+
setModel: (model) => setChoice((current) => ({ ...current, model })),
|
|
393
|
+
setReasoningEffort: (reasoningEffort) =>
|
|
394
|
+
setChoice((current) => ({ ...current, reasoningEffort })),
|
|
395
|
+
setLatencyMode: (latencyMode) => setChoice((current) => ({ ...current, latencyMode })),
|
|
396
|
+
draftPersistence: "disabled",
|
|
397
|
+
applyDraft: () => {},
|
|
398
|
+
reloadDraft: NOOP_ASYNC,
|
|
399
|
+
resolveDraftConflict: NOOP_ASYNC,
|
|
400
|
+
restoredResources: [],
|
|
401
|
+
removeRestoredResource: () => {},
|
|
402
|
+
// Shown below with the product's own copy.
|
|
403
|
+
error: null,
|
|
404
|
+
clearError: () => setError(null),
|
|
405
|
+
};
|
|
406
|
+
|
|
274
407
|
return (
|
|
275
408
|
<form
|
|
276
|
-
onSubmit={(event) =>
|
|
409
|
+
onSubmit={(event: FormEvent) => {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
void submit();
|
|
412
|
+
}}
|
|
277
413
|
className="mx-auto box-border flex h-full w-full max-w-3xl flex-col gap-4 p-3"
|
|
278
414
|
data-og-new-chat-composer=""
|
|
279
415
|
>
|
|
@@ -282,46 +418,43 @@ function NewChat({
|
|
|
282
418
|
{labels.newChatTitle ? (
|
|
283
419
|
<p className="text-center text-og-md font-medium text-og-fg">{labels.newChatTitle}</p>
|
|
284
420
|
) : null}
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)}
|
|
323
|
-
</button>
|
|
324
|
-
</div>
|
|
421
|
+
<ChatComposer
|
|
422
|
+
{...composerProps}
|
|
423
|
+
composer={composer}
|
|
424
|
+
attachments={uploadsEnabled ? files : undefined}
|
|
425
|
+
runControl="none"
|
|
426
|
+
running={false}
|
|
427
|
+
placeholder={placeholderOverride ?? composerProps?.placeholder ?? labels.newChatPlaceholder}
|
|
428
|
+
inputProps={{
|
|
429
|
+
"aria-label": placeholderOverride ?? labels.newChatPlaceholder,
|
|
430
|
+
...composerProps?.inputProps,
|
|
431
|
+
}}
|
|
432
|
+
messages={{ sendMessageAriaLabel: labels.send, ...composerProps?.messages }}
|
|
433
|
+
controlsStart={
|
|
434
|
+
composerProps?.controlsStart ??
|
|
435
|
+
(policy ? (
|
|
436
|
+
<ModelPolicyPicker
|
|
437
|
+
groupPresentation={modelPickerProps?.groupPresentation}
|
|
438
|
+
messages={modelPickerProps?.messages}
|
|
439
|
+
rows={catalog.rows}
|
|
440
|
+
model={policy.model}
|
|
441
|
+
effort={policy.reasoningEffort}
|
|
442
|
+
latencyMode={policy.latencyMode}
|
|
443
|
+
loading={catalog.loading}
|
|
444
|
+
error={catalog.error?.message}
|
|
445
|
+
disabled={sending}
|
|
446
|
+
menuSide="bottom"
|
|
447
|
+
onOpenChange={(open) => {
|
|
448
|
+
if (open) void catalog.refresh();
|
|
449
|
+
}}
|
|
450
|
+
onModelChange={composer.setModel!}
|
|
451
|
+
onEffortChange={composer.setReasoningEffort!}
|
|
452
|
+
onLatencyModeChange={composer.setLatencyMode!}
|
|
453
|
+
/>
|
|
454
|
+
) : undefined)
|
|
455
|
+
}
|
|
456
|
+
responsiveBasis={composerProps?.responsiveBasis ?? "container"}
|
|
457
|
+
/>
|
|
325
458
|
{error ? (
|
|
326
459
|
<p role="alert" className="text-center text-og-sm text-og-status-failed">
|
|
327
460
|
{formatError(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SESSION_SCOPE_HEADER, type SendMessageInput } from "@opengeni/sdk";
|
|
1
|
+
import { SESSION_SCOPE_HEADER, type ReasoningEffort, type SendMessageInput } from "@opengeni/sdk";
|
|
2
2
|
import {
|
|
3
3
|
lazy,
|
|
4
4
|
Suspense,
|
|
@@ -134,11 +134,22 @@ export type SessionConversationProps = ClientOverride &
|
|
|
134
134
|
* (including Site clients), one shared event feed, and authoritative queue state.
|
|
135
135
|
* `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />` needs no
|
|
136
136
|
* provider: it talks to your session proxy and its resolved workspace. */
|
|
137
|
-
export function SessionConversation({
|
|
137
|
+
export function SessionConversation({
|
|
138
|
+
baseUrl,
|
|
139
|
+
headers,
|
|
140
|
+
fetch,
|
|
141
|
+
...props
|
|
142
|
+
}: SessionConversationProps) {
|
|
138
143
|
if (baseUrl === undefined) return <RetryingConversation {...props} />;
|
|
139
|
-
const { client
|
|
144
|
+
const { client, workspaceId, ...rest } = props;
|
|
140
145
|
return (
|
|
141
|
-
<SessionProxyScope
|
|
146
|
+
<SessionProxyScope
|
|
147
|
+
baseUrl={baseUrl}
|
|
148
|
+
workspaceId={workspaceId}
|
|
149
|
+
client={client}
|
|
150
|
+
headers={headers}
|
|
151
|
+
fetch={fetch}
|
|
152
|
+
>
|
|
142
153
|
<RetryingConversation {...rest} />
|
|
143
154
|
</SessionProxyScope>
|
|
144
155
|
);
|
|
@@ -494,17 +505,24 @@ function useDefaultInteractiveBlock(
|
|
|
494
505
|
}
|
|
495
506
|
|
|
496
507
|
/** Deployment/proxy flags from the client config: uploads, and whether model choice is open. */
|
|
497
|
-
function useClientConfigFlags(client: {
|
|
508
|
+
export function useClientConfigFlags(client: {
|
|
498
509
|
getClientConfig: () => Promise<{
|
|
499
510
|
fileUploads?: { enabled?: boolean };
|
|
500
511
|
modelSelection?: boolean | undefined;
|
|
501
512
|
sandboxFiles?: boolean | undefined;
|
|
513
|
+
defaultReasoningEffort?: ReasoningEffort | undefined;
|
|
502
514
|
}>;
|
|
503
|
-
}): {
|
|
504
|
-
|
|
515
|
+
}): {
|
|
516
|
+
uploads: boolean;
|
|
517
|
+
modelSelection: boolean;
|
|
518
|
+
sandboxFiles: boolean;
|
|
519
|
+
defaultReasoningEffort: ReasoningEffort | null;
|
|
520
|
+
} {
|
|
521
|
+
const [flags, setFlags] = useState<ReturnType<typeof useClientConfigFlags>>({
|
|
505
522
|
uploads: false,
|
|
506
523
|
modelSelection: false,
|
|
507
524
|
sandboxFiles: false,
|
|
525
|
+
defaultReasoningEffort: null,
|
|
508
526
|
});
|
|
509
527
|
useEffect(() => {
|
|
510
528
|
let live = true;
|
|
@@ -516,6 +534,7 @@ function useClientConfigFlags(client: {
|
|
|
516
534
|
// Only an explicit offer shows end users the picker.
|
|
517
535
|
modelSelection: config.modelSelection === true,
|
|
518
536
|
sandboxFiles: config.sandboxFiles !== false,
|
|
537
|
+
defaultReasoningEffort: config.defaultReasoningEffort ?? null,
|
|
519
538
|
});
|
|
520
539
|
}
|
|
521
540
|
},
|
|
@@ -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
|
);
|