@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
|
@@ -203,7 +203,7 @@ function EditableBody({
|
|
|
203
203
|
workspaceId: string;
|
|
204
204
|
sessionId: string;
|
|
205
205
|
result:
|
|
206
|
-
| { kind: "ready"; config: NonNullable<ClientConfig["artifacts"]> }
|
|
206
|
+
| { kind: "ready"; config: Exclude<NonNullable<ClientConfig["artifacts"]>, false> }
|
|
207
207
|
| { kind: "unavailable" }
|
|
208
208
|
| { kind: "error"; error: unknown };
|
|
209
209
|
} | null>(null);
|
|
@@ -359,7 +359,9 @@ function EditableBody({
|
|
|
359
359
|
);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
function artifactCapabilityKey(
|
|
362
|
+
function artifactCapabilityKey(
|
|
363
|
+
config: Exclude<NonNullable<ClientConfig["artifacts"]>, false>,
|
|
364
|
+
): string {
|
|
363
365
|
return JSON.stringify([
|
|
364
366
|
config.editableLiveUrl,
|
|
365
367
|
config.cachePartition.accountId,
|
|
@@ -50,6 +50,9 @@ export type ComposerTranscriptionMessages = {
|
|
|
50
50
|
errorPermissionDenied: string;
|
|
51
51
|
errorNotSupported: string;
|
|
52
52
|
errorUnavailable: string;
|
|
53
|
+
errorInsufficientCredits: string;
|
|
54
|
+
errorAllowanceExhausted: string;
|
|
55
|
+
errorPolicyBlocked: string;
|
|
53
56
|
errorTooLarge: string;
|
|
54
57
|
errorInvalidAudio: string;
|
|
55
58
|
errorStorageUnavailable: string;
|
|
@@ -78,6 +81,10 @@ const defaultMessages: ComposerTranscriptionMessages = {
|
|
|
78
81
|
errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
|
|
79
82
|
errorNotSupported: "Voice input is not supported on this device.",
|
|
80
83
|
errorUnavailable: "Voice input is not configured.",
|
|
84
|
+
errorInsufficientCredits:
|
|
85
|
+
"Voice input needs Opengeni credits. Add credits, then retry your saved recording.",
|
|
86
|
+
errorAllowanceExhausted: "Your usage limit was reached. Your recording is saved for later.",
|
|
87
|
+
errorPolicyBlocked: "Voice input isn't allowed for this account or workspace.",
|
|
81
88
|
errorTooLarge: "Recording is too large. Try a shorter message.",
|
|
82
89
|
errorInvalidAudio: "The recording could not be read. Try again.",
|
|
83
90
|
errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
|
|
@@ -518,6 +525,13 @@ function restingLevels(count: number): number[] {
|
|
|
518
525
|
|
|
519
526
|
function transcriptionErrorMessage(code: string, messages: ComposerTranscriptionMessages): string {
|
|
520
527
|
switch (code) {
|
|
528
|
+
case "insufficient_credits":
|
|
529
|
+
return messages.errorInsufficientCredits;
|
|
530
|
+
case "allowance_exhausted":
|
|
531
|
+
case "monthly_model_cost_limit":
|
|
532
|
+
return messages.errorAllowanceExhausted;
|
|
533
|
+
case "policy_blocked":
|
|
534
|
+
return messages.errorPolicyBlocked;
|
|
521
535
|
case "permission_denied":
|
|
522
536
|
return messages.errorPermissionDenied;
|
|
523
537
|
case "not_supported":
|
|
@@ -967,6 +967,7 @@ export const Surface = forwardRef<HTMLDivElement, ComposerSurfaceProps>(function
|
|
|
967
967
|
const controller = useComposerController();
|
|
968
968
|
return (
|
|
969
969
|
<div
|
|
970
|
+
data-og-composer-surface=""
|
|
970
971
|
{...props}
|
|
971
972
|
ref={ref}
|
|
972
973
|
onDragOver={controller.attachments ? controller.handleDragOver : undefined}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniApiError,
|
|
3
|
+
type FileResourceRef,
|
|
4
|
+
type LatencyMode,
|
|
5
|
+
type OpenGeniClient,
|
|
6
|
+
type ReasoningEffort,
|
|
7
|
+
} from "@opengeni/sdk";
|
|
8
|
+
import { MenuIcon, XIcon } from "lucide-react";
|
|
3
9
|
import { useCallback, useRef, useState, type CSSProperties, type FormEvent } from "react";
|
|
10
|
+
import { useWorkspaceModelCatalog } from "../hooks/use-available-models";
|
|
11
|
+
import { FILE_ONLY_MESSAGE_TEXT, type ComposerState } from "../hooks/use-composer";
|
|
12
|
+
import { useFileAttachments } from "../hooks/use-file-attachments";
|
|
4
13
|
import { cn } from "../lib/cn";
|
|
5
14
|
import { useErrorMessage } from "../lib/error-message";
|
|
6
15
|
import {
|
|
@@ -9,7 +18,14 @@ import {
|
|
|
9
18
|
type HostThemePreference,
|
|
10
19
|
} from "../lib/host-theme";
|
|
11
20
|
import { useOpenGeni, type ClientOverride } from "../session-context";
|
|
12
|
-
import {
|
|
21
|
+
import { ChatComposer } from "./chat-composer";
|
|
22
|
+
import { ModelPolicyPicker } from "./model-policy-picker";
|
|
23
|
+
import { useClientConfigFlags } from "../hooks/use-client-config-flags";
|
|
24
|
+
import {
|
|
25
|
+
SessionConversation,
|
|
26
|
+
useComposerTranscription,
|
|
27
|
+
type SessionConversationProps,
|
|
28
|
+
} from "./session-conversation";
|
|
13
29
|
import { SessionList, type SessionListLabels, type SessionListProps } from "./session-list";
|
|
14
30
|
import { SessionProxyScope, type SessionProxyBaseUrl } from "./session-proxy-scope";
|
|
15
31
|
|
|
@@ -32,6 +48,16 @@ const DEFAULT_LABELS: Omit<OpenGeniChatLabels, keyof SessionListLabels> = {
|
|
|
32
48
|
newChatUnavailable: "New chats are not enabled for this product.",
|
|
33
49
|
};
|
|
34
50
|
|
|
51
|
+
/** What the first message carries besides its text. */
|
|
52
|
+
export type OpenGeniChatCreateOptions = {
|
|
53
|
+
/** Files attached in the new-chat composer. */
|
|
54
|
+
resources?: FileResourceRef[] | undefined;
|
|
55
|
+
/** Present only when the user changed it in the model picker. */
|
|
56
|
+
model?: string | undefined;
|
|
57
|
+
reasoningEffort?: ReasoningEffort | undefined;
|
|
58
|
+
latencyMode?: LatencyMode | undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
35
61
|
export type OpenGeniChatProps = ClientOverride &
|
|
36
62
|
SessionProxyBaseUrl & {
|
|
37
63
|
/** Controlled selection. `null` shows the new-chat composer. */
|
|
@@ -41,18 +67,31 @@ export type OpenGeniChatProps = ClientOverride &
|
|
|
41
67
|
onSessionChange?: ((sessionId: string | null) => void) | undefined;
|
|
42
68
|
/**
|
|
43
69
|
* Create a chat from its first message. Defaults to `client.createSession`
|
|
44
|
-
* with `{ initialMessage, idempotencyKey }
|
|
45
|
-
*
|
|
70
|
+
* with `{ initialMessage, idempotencyKey }` plus the attached files and any
|
|
71
|
+
* model choice, which `createSessionProxyHandler` accepts when the server
|
|
72
|
+
* supplies a `createSession` hook. Return the new id.
|
|
46
73
|
*/
|
|
47
74
|
createSession?:
|
|
48
|
-
| ((
|
|
75
|
+
| ((
|
|
76
|
+
initialMessage: string,
|
|
77
|
+
idempotencyKey: string,
|
|
78
|
+
options: OpenGeniChatCreateOptions,
|
|
79
|
+
) => Promise<string>)
|
|
49
80
|
| undefined;
|
|
50
|
-
/**
|
|
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
|
+
*/
|
|
51
86
|
newChat?: boolean | undefined;
|
|
52
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Forwarded to the conversation (message rendering, tool renderers,
|
|
89
|
+
* composer...). `attachments`, `modelPicker`, `modelPickerProps`, and
|
|
90
|
+
* `composerProps` also apply to the new-chat composer.
|
|
91
|
+
*/
|
|
53
92
|
conversationProps?: Omit<
|
|
54
93
|
SessionConversationProps,
|
|
55
|
-
"sessionId" | "client" | "workspaceId" | "baseUrl"
|
|
94
|
+
"sessionId" | "client" | "workspaceId" | "baseUrl" | "headers" | "fetch"
|
|
56
95
|
>;
|
|
57
96
|
/** Forwarded to the list (rename/archive toggles, page size). */
|
|
58
97
|
listProps?: Pick<SessionListProps, "rename" | "archive" | "pageSize"> | undefined;
|
|
@@ -83,11 +122,17 @@ type CreateClient = Partial<Pick<OpenGeniClient, "createSession">>;
|
|
|
83
122
|
* `<OpenGeniChat baseUrl="/api/opengeni" />` needs no provider: it talks to
|
|
84
123
|
* your session proxy and uses the workspace the proxy resolves.
|
|
85
124
|
*/
|
|
86
|
-
export function OpenGeniChat({ baseUrl, ...props }: OpenGeniChatProps) {
|
|
125
|
+
export function OpenGeniChat({ baseUrl, headers, fetch, ...props }: OpenGeniChatProps) {
|
|
87
126
|
if (baseUrl === undefined) return <Chat {...props} />;
|
|
88
|
-
const { client
|
|
127
|
+
const { client, workspaceId, ...rest } = props;
|
|
89
128
|
return (
|
|
90
|
-
<SessionProxyScope
|
|
129
|
+
<SessionProxyScope
|
|
130
|
+
baseUrl={baseUrl}
|
|
131
|
+
workspaceId={workspaceId}
|
|
132
|
+
client={client}
|
|
133
|
+
headers={headers}
|
|
134
|
+
fetch={fetch}
|
|
135
|
+
>
|
|
91
136
|
<Chat {...rest} />
|
|
92
137
|
</SessionProxyScope>
|
|
93
138
|
);
|
|
@@ -108,7 +153,7 @@ function Chat({
|
|
|
108
153
|
height = "100%",
|
|
109
154
|
theme,
|
|
110
155
|
surface,
|
|
111
|
-
}: Omit<OpenGeniChatProps, "baseUrl">) {
|
|
156
|
+
}: Omit<OpenGeniChatProps, "baseUrl" | "headers" | "fetch">) {
|
|
112
157
|
const labels = { ...DEFAULT_LABELS, ...labelOverrides } as OpenGeniChatLabels;
|
|
113
158
|
const scope = { client, workspaceId };
|
|
114
159
|
const context = useOpenGeni(scope);
|
|
@@ -118,6 +163,9 @@ function Chat({
|
|
|
118
163
|
const [listRevision, setListRevision] = useState(0);
|
|
119
164
|
const root = useRef<HTMLDivElement>(null);
|
|
120
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;
|
|
121
169
|
|
|
122
170
|
const select = useCallback(
|
|
123
171
|
(next: string | null) => {
|
|
@@ -129,15 +177,28 @@ function Chat({
|
|
|
129
177
|
);
|
|
130
178
|
|
|
131
179
|
const create = useCallback(
|
|
132
|
-
async (
|
|
133
|
-
|
|
180
|
+
async (
|
|
181
|
+
initialMessage: string,
|
|
182
|
+
idempotencyKey: string,
|
|
183
|
+
options: OpenGeniChatCreateOptions,
|
|
184
|
+
): Promise<string> => {
|
|
185
|
+
if (createSession) return await createSession(initialMessage, idempotencyKey, options);
|
|
134
186
|
const creator = (context.client as unknown as CreateClient).createSession;
|
|
135
187
|
if (typeof creator !== "function") throw new Error(labels.newChatUnavailable);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
202
|
},
|
|
142
203
|
[context.client, context.workspaceId, createSession, labels.newChatUnavailable],
|
|
143
204
|
);
|
|
@@ -149,7 +210,7 @@ function Chat({
|
|
|
149
210
|
labels={labels}
|
|
150
211
|
selectedSessionId={selected}
|
|
151
212
|
onSelect={select}
|
|
152
|
-
onNewChat={newChat ? () => select(null) : undefined}
|
|
213
|
+
onNewChat={newChat && canCreate ? () => select(null) : undefined}
|
|
153
214
|
onArchived={(archivedId) => {
|
|
154
215
|
if (archivedId === selected) select(null);
|
|
155
216
|
}}
|
|
@@ -217,13 +278,19 @@ function Chat({
|
|
|
217
278
|
{selected ? (
|
|
218
279
|
<SessionConversation
|
|
219
280
|
{...conversationProps}
|
|
281
|
+
// Sub-agent chats open in place, like a chat from the list.
|
|
282
|
+
onOpenSession={conversationProps?.onOpenSession ?? select}
|
|
220
283
|
{...scope}
|
|
221
284
|
sessionId={selected}
|
|
222
285
|
height="100%"
|
|
223
286
|
/>
|
|
224
287
|
) : (
|
|
225
288
|
<NewChat
|
|
289
|
+
scope={scope}
|
|
226
290
|
labels={labels}
|
|
291
|
+
placeholderOverride={labelOverrides?.newChatPlaceholder}
|
|
292
|
+
conversationProps={conversationProps}
|
|
293
|
+
available={canCreate}
|
|
227
294
|
create={create}
|
|
228
295
|
onCreated={(id) => {
|
|
229
296
|
setListRevision((revision) => revision + 1);
|
|
@@ -237,43 +304,146 @@ function Chat({
|
|
|
237
304
|
);
|
|
238
305
|
}
|
|
239
306
|
|
|
307
|
+
const NOOP_ASYNC = async () => {};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The first message of a new chat, in the same composer as follow-ups: file
|
|
311
|
+
* attachments, the model picker when offered, and the host's `composerProps`
|
|
312
|
+
* (custom controls, voice input, copy).
|
|
313
|
+
*/
|
|
240
314
|
function NewChat({
|
|
315
|
+
scope,
|
|
241
316
|
labels,
|
|
317
|
+
placeholderOverride,
|
|
318
|
+
conversationProps,
|
|
319
|
+
available,
|
|
242
320
|
create,
|
|
243
321
|
onCreated,
|
|
244
322
|
}: {
|
|
323
|
+
scope: ClientOverride;
|
|
245
324
|
labels: OpenGeniChatLabels;
|
|
246
|
-
|
|
325
|
+
placeholderOverride: string | undefined;
|
|
326
|
+
conversationProps: OpenGeniChatProps["conversationProps"];
|
|
327
|
+
/** False when this product cannot start chats; the composer is disabled and says so. */
|
|
328
|
+
available: boolean;
|
|
329
|
+
create: (
|
|
330
|
+
initialMessage: string,
|
|
331
|
+
idempotencyKey: string,
|
|
332
|
+
options: OpenGeniChatCreateOptions,
|
|
333
|
+
) => Promise<string>;
|
|
247
334
|
onCreated: (sessionId: string) => void;
|
|
248
335
|
}) {
|
|
336
|
+
const context = useOpenGeni(scope);
|
|
337
|
+
const config = useClientConfigFlags(context.client);
|
|
338
|
+
const composerProps = conversationProps?.composerProps;
|
|
339
|
+
const modelPickerProps = conversationProps?.modelPickerProps;
|
|
340
|
+
const showModelPicker = conversationProps?.modelPicker ?? config.modelSelection;
|
|
341
|
+
const catalog = useWorkspaceModelCatalog({
|
|
342
|
+
client: context.client,
|
|
343
|
+
workspaceId: context.workspaceId,
|
|
344
|
+
enabled: showModelPicker,
|
|
345
|
+
});
|
|
346
|
+
const files = useFileAttachments(scope);
|
|
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
|
+
);
|
|
249
353
|
const [text, setText] = useState("");
|
|
250
354
|
const [sending, setSending] = useState(false);
|
|
251
355
|
const [error, setError] = useState<{ cause: unknown } | null>(null);
|
|
356
|
+
// Only what the user changed is sent; everything else stays server-owned.
|
|
357
|
+
const [choice, setChoice] = useState<
|
|
358
|
+
Pick<OpenGeniChatCreateOptions, "model" | "reasoningEffort" | "latencyMode">
|
|
359
|
+
>({});
|
|
252
360
|
const formatError = useErrorMessage();
|
|
253
|
-
// One key per
|
|
254
|
-
|
|
361
|
+
// One key per attempted payload, so a retried send returns the same chat
|
|
362
|
+
// and an edited retry is a new request rather than an idempotency conflict.
|
|
363
|
+
const attempt = useRef<{ fingerprint: string; key: string } | null>(null);
|
|
364
|
+
const resources = uploadsEnabled ? files.readyResources : [];
|
|
365
|
+
const pendingFiles = uploadsEnabled && files.hasUnresolved;
|
|
255
366
|
|
|
256
|
-
const submit = async (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
367
|
+
const submit = async (): Promise<boolean> => {
|
|
368
|
+
const initialMessage = text.trim() || (resources.length > 0 ? FILE_ONLY_MESSAGE_TEXT : "");
|
|
369
|
+
if (!initialMessage || sending || pendingFiles || !available) return false;
|
|
370
|
+
const options: OpenGeniChatCreateOptions = {
|
|
371
|
+
...(resources.length > 0 ? { resources } : {}),
|
|
372
|
+
...(showModelPicker ? choice : {}),
|
|
373
|
+
};
|
|
374
|
+
const fingerprint = JSON.stringify([initialMessage, options]);
|
|
375
|
+
if (attempt.current?.fingerprint !== fingerprint) {
|
|
376
|
+
attempt.current = { fingerprint, key: crypto.randomUUID() };
|
|
377
|
+
}
|
|
260
378
|
setSending(true);
|
|
261
379
|
setError(null);
|
|
262
380
|
try {
|
|
263
|
-
const id = await create(initialMessage,
|
|
264
|
-
|
|
381
|
+
const id = await create(initialMessage, attempt.current.key, options);
|
|
382
|
+
attempt.current = null;
|
|
265
383
|
setText("");
|
|
384
|
+
files.removeReadyFiles(resources.map((resource) => resource.fileId));
|
|
266
385
|
onCreated(id);
|
|
386
|
+
return true;
|
|
267
387
|
} catch (cause) {
|
|
268
388
|
setError({ cause });
|
|
389
|
+
return false;
|
|
269
390
|
} finally {
|
|
270
391
|
setSending(false);
|
|
271
392
|
}
|
|
272
393
|
};
|
|
273
394
|
|
|
395
|
+
const defaultModel = catalog.defaultModel;
|
|
396
|
+
const policy =
|
|
397
|
+
showModelPicker && (choice.model ?? defaultModel)
|
|
398
|
+
? {
|
|
399
|
+
model: (choice.model ?? defaultModel)!,
|
|
400
|
+
reasoningEffort:
|
|
401
|
+
choice.reasoningEffort ?? config.defaultReasoningEffort ?? ("medium" as const),
|
|
402
|
+
latencyMode: choice.latencyMode ?? ("standard" as const),
|
|
403
|
+
}
|
|
404
|
+
: null;
|
|
405
|
+
|
|
406
|
+
const composer: ComposerState = {
|
|
407
|
+
value: text,
|
|
408
|
+
setValue: setText,
|
|
409
|
+
hasDraftContent: () => text.length > 0 || files.attachments.length > 0,
|
|
410
|
+
send: submit,
|
|
411
|
+
steer: submit,
|
|
412
|
+
sending,
|
|
413
|
+
canSend:
|
|
414
|
+
available && (text.trim().length > 0 || resources.length > 0) && !sending && !pendingFiles,
|
|
415
|
+
pause: NOOP_ASYNC,
|
|
416
|
+
pausing: false,
|
|
417
|
+
resume: NOOP_ASYNC,
|
|
418
|
+
resumeScope: NOOP_ASYNC,
|
|
419
|
+
resuming: false,
|
|
420
|
+
draft: null,
|
|
421
|
+
draftRevision: 0,
|
|
422
|
+
draftLoading: false,
|
|
423
|
+
draftSaving: false,
|
|
424
|
+
draftConflict: null,
|
|
425
|
+
policy,
|
|
426
|
+
setModel: (model) => setChoice((current) => ({ ...current, model })),
|
|
427
|
+
setReasoningEffort: (reasoningEffort) =>
|
|
428
|
+
setChoice((current) => ({ ...current, reasoningEffort })),
|
|
429
|
+
setLatencyMode: (latencyMode) => setChoice((current) => ({ ...current, latencyMode })),
|
|
430
|
+
draftPersistence: "disabled",
|
|
431
|
+
applyDraft: () => {},
|
|
432
|
+
reloadDraft: NOOP_ASYNC,
|
|
433
|
+
resolveDraftConflict: NOOP_ASYNC,
|
|
434
|
+
restoredResources: [],
|
|
435
|
+
removeRestoredResource: () => {},
|
|
436
|
+
// Shown below with the product's own copy.
|
|
437
|
+
error: null,
|
|
438
|
+
clearError: () => setError(null),
|
|
439
|
+
};
|
|
440
|
+
|
|
274
441
|
return (
|
|
275
442
|
<form
|
|
276
|
-
onSubmit={(event) =>
|
|
443
|
+
onSubmit={(event: FormEvent) => {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
void submit();
|
|
446
|
+
}}
|
|
277
447
|
className="mx-auto box-border flex h-full w-full max-w-3xl flex-col gap-4 p-3"
|
|
278
448
|
data-og-new-chat-composer=""
|
|
279
449
|
>
|
|
@@ -282,47 +452,50 @@ function NewChat({
|
|
|
282
452
|
{labels.newChatTitle ? (
|
|
283
453
|
<p className="text-center text-og-md font-medium text-og-fg">{labels.newChatTitle}</p>
|
|
284
454
|
) : 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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
455
|
+
<ChatComposer
|
|
456
|
+
{...composerProps}
|
|
457
|
+
{...(transcription && composerProps?.transcription === undefined ? { transcription } : {})}
|
|
458
|
+
composer={composer}
|
|
459
|
+
attachments={uploadsEnabled ? files : undefined}
|
|
460
|
+
disabled={!available || composerProps?.disabled}
|
|
461
|
+
runControl="none"
|
|
462
|
+
running={false}
|
|
463
|
+
placeholder={placeholderOverride ?? composerProps?.placeholder ?? labels.newChatPlaceholder}
|
|
464
|
+
inputProps={{
|
|
465
|
+
"aria-label": placeholderOverride ?? labels.newChatPlaceholder,
|
|
466
|
+
...composerProps?.inputProps,
|
|
467
|
+
}}
|
|
468
|
+
messages={{ sendMessageAriaLabel: labels.send, ...composerProps?.messages }}
|
|
469
|
+
controlsStart={
|
|
470
|
+
composerProps?.controlsStart ??
|
|
471
|
+
(policy ? (
|
|
472
|
+
<ModelPolicyPicker
|
|
473
|
+
groupPresentation={modelPickerProps?.groupPresentation}
|
|
474
|
+
messages={modelPickerProps?.messages}
|
|
475
|
+
rows={catalog.rows}
|
|
476
|
+
model={policy.model}
|
|
477
|
+
effort={policy.reasoningEffort}
|
|
478
|
+
latencyMode={policy.latencyMode}
|
|
479
|
+
loading={catalog.loading}
|
|
480
|
+
error={catalog.error?.message}
|
|
481
|
+
disabled={sending}
|
|
482
|
+
menuSide="bottom"
|
|
483
|
+
onOpenChange={(open) => {
|
|
484
|
+
if (open) void catalog.refresh();
|
|
485
|
+
}}
|
|
486
|
+
onModelChange={composer.setModel!}
|
|
487
|
+
onEffortChange={composer.setReasoningEffort!}
|
|
488
|
+
onLatencyModeChange={composer.setLatencyMode!}
|
|
489
|
+
/>
|
|
490
|
+
) : undefined)
|
|
491
|
+
}
|
|
492
|
+
responsiveBasis={composerProps?.responsiveBasis ?? "container"}
|
|
493
|
+
/>
|
|
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 ? (
|
|
326
499
|
<p role="alert" className="text-center text-og-sm text-og-status-failed">
|
|
327
500
|
{formatError(
|
|
328
501
|
error.cause,
|