@opengeni/react 0.42.1 → 0.44.1
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/dist/{chunk-SJKT4TKW.js → chunk-23EJ676W.js} +3 -1
- package/dist/chunk-23EJ676W.js.map +1 -0
- package/dist/{chunk-UWYTCQWW.js → chunk-KC27K42G.js} +92 -21
- package/dist/{chunk-UWYTCQWW.js.map → chunk-KC27K42G.js.map} +1 -1
- package/dist/{chunk-KR2SK5GJ.js → chunk-KG5F2OKE.js} +260 -93
- package/dist/chunk-KG5F2OKE.js.map +1 -0
- package/dist/{chunk-4IJCL7YO.js → chunk-LWR4MXSS.js} +4 -1
- package/dist/chunk-LWR4MXSS.js.map +1 -0
- package/dist/{chunk-JALF5FI3.js → chunk-SRFUT2ZU.js} +2 -2
- package/dist/{chunk-WZT5G5OR.js → chunk-U6K24XQD.js} +5 -4
- package/dist/{chunk-WZT5G5OR.js.map → chunk-U6K24XQD.js.map} +1 -1
- package/dist/components/chat-composer.d.ts +5 -1
- package/dist/components/composer-transcription-control.d.ts +3 -1
- package/dist/components/composer.d.ts +6 -4
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -20
- package/dist/index.js.map +1 -1
- package/dist/model-policy.d.ts +2 -0
- package/dist/model-policy.js +1 -1
- package/dist/realtime/realtime-control.d.ts +29 -1
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.js +269 -151
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +2 -2
- package/dist/session.js +2 -2
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/parsers.d.ts +13 -8
- package/package.json +2 -2
- package/src/components/chat-composer.tsx +58 -19
- package/src/components/composer-transcription-control.tsx +194 -165
- package/src/components/composer.tsx +78 -14
- package/src/components/model-policy-picker.tsx +7 -2
- package/src/components/session-chrome.tsx +89 -78
- package/src/index.ts +2 -0
- package/src/model-policy.ts +4 -0
- package/src/realtime/realtime-control.tsx +307 -137
- package/src/realtime.ts +1 -0
- package/src/timeline/index.ts +2 -0
- package/src/timeline/parsers.ts +201 -19
- package/src/timeline/projection.ts +11 -0
- package/src/timeline/tool-renderers.tsx +7 -5
- package/src/timeline/turn-summary.tsx +7 -2
- package/styles/tokens.css +8 -5
- package/dist/chunk-4IJCL7YO.js.map +0 -1
- package/dist/chunk-KR2SK5GJ.js.map +0 -1
- package/dist/chunk-SJKT4TKW.js.map +0 -1
- /package/dist/{chunk-JALF5FI3.js.map → chunk-SRFUT2ZU.js.map} +0 -0
|
@@ -101,6 +101,8 @@ export type ComposerTranscriptionControlProps = {
|
|
|
101
101
|
createRecordingStore?: (() => VoiceRecordingStore) | undefined;
|
|
102
102
|
/** Test/embed seam. Production acquires a coordinated browser-document owner lease. */
|
|
103
103
|
createOwnerId?: (() => string) | undefined;
|
|
104
|
+
/** Hide while realtime voice owns the mic — collapses with a layout-friendly exit. */
|
|
105
|
+
suppressed?: boolean | undefined;
|
|
104
106
|
};
|
|
105
107
|
|
|
106
108
|
const WAVEFORM_BARS = 18;
|
|
@@ -119,6 +121,7 @@ export function ComposerTranscriptionControl({
|
|
|
119
121
|
className,
|
|
120
122
|
createRecordingStore,
|
|
121
123
|
createOwnerId,
|
|
124
|
+
suppressed = false,
|
|
122
125
|
}: ComposerTranscriptionControlProps) {
|
|
123
126
|
const composer = useChatComposer();
|
|
124
127
|
const messages = { ...defaultMessages, ...overrides };
|
|
@@ -126,7 +129,7 @@ export function ComposerTranscriptionControl({
|
|
|
126
129
|
client,
|
|
127
130
|
workspaceId,
|
|
128
131
|
capability,
|
|
129
|
-
enabled: workspaceEnabled,
|
|
132
|
+
enabled: workspaceEnabled && !suppressed,
|
|
130
133
|
value: composer.value,
|
|
131
134
|
setValue: composer.setValue,
|
|
132
135
|
focusInput: composer.focusInput,
|
|
@@ -135,6 +138,14 @@ export function ComposerTranscriptionControl({
|
|
|
135
138
|
createOwnerId,
|
|
136
139
|
});
|
|
137
140
|
const { status } = transcription;
|
|
141
|
+
|
|
142
|
+
const cancelTranscription = transcription.cancel;
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!suppressed) return;
|
|
145
|
+
if (status === "recording" || status === "requesting-permission") {
|
|
146
|
+
cancelTranscription();
|
|
147
|
+
}
|
|
148
|
+
}, [cancelTranscription, status, suppressed]);
|
|
138
149
|
const active =
|
|
139
150
|
status === "requesting-permission" ||
|
|
140
151
|
status === "recording" ||
|
|
@@ -181,179 +192,197 @@ export function ComposerTranscriptionControl({
|
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
return (
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
195
|
+
<AnimatePresence initial={false}>
|
|
196
|
+
{suppressed ? null : (
|
|
197
|
+
<motion.span
|
|
198
|
+
key="composer-dictate"
|
|
199
|
+
initial={{ opacity: 0, width: 0 }}
|
|
200
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
201
|
+
exit={{ opacity: 0, width: 0 }}
|
|
202
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
203
|
+
className={cn("inline-flex min-w-0 overflow-hidden", className)}
|
|
204
|
+
data-transcription-status={status}
|
|
205
|
+
>
|
|
206
|
+
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
207
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
208
|
+
{recoverable ? (
|
|
209
|
+
<motion.span
|
|
210
|
+
key="recovered"
|
|
211
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
212
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
213
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
214
|
+
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
215
|
+
className={cn(
|
|
216
|
+
"inline-flex h-8 min-w-0 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
217
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
<span className="max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
|
|
221
|
+
{savedTranscript
|
|
222
|
+
? (errorMessage ?? messages.recoveredTranscript)
|
|
223
|
+
: status === "error"
|
|
224
|
+
? (errorMessage ?? messages.errorRetryable)
|
|
225
|
+
: messages.recovered}
|
|
226
|
+
</span>
|
|
227
|
+
<Tip tip={savedTranscript ? messages.insertRecoveredTranscript : messages.retry}>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
onClick={() =>
|
|
231
|
+
savedTranscript
|
|
232
|
+
? void transcription.insertSavedTranscript()
|
|
233
|
+
: transcription.retry()
|
|
234
|
+
}
|
|
235
|
+
aria-label={
|
|
236
|
+
savedTranscript ? messages.insertRecoveredTranscript : messages.retry
|
|
237
|
+
}
|
|
238
|
+
className={cn(
|
|
239
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
240
|
+
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
241
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
{savedTranscript ? (
|
|
245
|
+
<ClipboardPasteIcon className="size-3.5" />
|
|
246
|
+
) : (
|
|
247
|
+
<RefreshCwIcon className="size-3.5" />
|
|
248
|
+
)}
|
|
249
|
+
</button>
|
|
250
|
+
</Tip>
|
|
251
|
+
<Tip tip={messages.discardRecovered}>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={() => void transcription.discard()}
|
|
255
|
+
aria-label={messages.discardRecovered}
|
|
256
|
+
className={cn(
|
|
257
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
258
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
259
|
+
"hover:bg-og-surface-3 hover:text-og-status-failed pointer-coarse:size-11",
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<Trash2Icon className="size-3.5" />
|
|
263
|
+
</button>
|
|
264
|
+
</Tip>
|
|
265
|
+
</motion.span>
|
|
266
|
+
) : active ? (
|
|
267
|
+
<motion.span
|
|
268
|
+
key={status === "transcribing" || status === "saving" ? "processing" : "capture"}
|
|
269
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
270
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
271
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
272
|
+
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
273
|
+
className={cn(
|
|
274
|
+
"inline-flex h-8 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
275
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
{status === "requesting-permission" ? (
|
|
279
|
+
<LoaderCircleIcon
|
|
280
|
+
className="size-3.5 shrink-0 text-og-fg-muted animate-og-spin motion-reduce:animate-none"
|
|
281
|
+
aria-hidden
|
|
282
|
+
/>
|
|
283
|
+
) : (
|
|
284
|
+
<span className="flex items-center gap-1.5">
|
|
285
|
+
{status === "recording" ? (
|
|
286
|
+
<span
|
|
287
|
+
aria-hidden
|
|
288
|
+
className="size-1.5 shrink-0 rounded-full bg-og-status-failed animate-og-pulse motion-reduce:animate-none"
|
|
289
|
+
/>
|
|
290
|
+
) : null}
|
|
291
|
+
<VoiceWaveform
|
|
292
|
+
stream={status === "recording" ? transcription.stream : null}
|
|
293
|
+
mode={status === "recording" ? "recording" : "transcribing"}
|
|
294
|
+
/>
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
{status === "recording" ? (
|
|
298
|
+
<>
|
|
299
|
+
<Tip tip={messages.cancel}>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => transcription.cancel()}
|
|
303
|
+
aria-label={messages.cancel}
|
|
304
|
+
aria-keyshortcuts="Escape"
|
|
305
|
+
className={cn(
|
|
306
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
307
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
308
|
+
"hover:bg-og-surface-3 hover:text-og-fg pointer-coarse:size-11",
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
<XIcon className="size-3.5" />
|
|
312
|
+
</button>
|
|
313
|
+
</Tip>
|
|
314
|
+
<Tip tip={messages.stop}>
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={() => transcription.stop()}
|
|
318
|
+
aria-label={messages.stop}
|
|
319
|
+
className={cn(
|
|
320
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
321
|
+
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
322
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
<SquareIcon className="size-2.5 fill-current" />
|
|
326
|
+
</button>
|
|
327
|
+
</Tip>
|
|
328
|
+
</>
|
|
329
|
+
) : status === "transcribing" || status === "saving" ? (
|
|
330
|
+
<span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
|
|
331
|
+
{status === "saving" ? messages.saving : messages.transcribing}
|
|
332
|
+
</span>
|
|
333
|
+
) : (
|
|
334
|
+
<span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
|
|
335
|
+
{messages.requestingPermission}
|
|
336
|
+
</span>
|
|
337
|
+
)}
|
|
338
|
+
</motion.span>
|
|
339
|
+
) : (
|
|
340
|
+
<Tip key="idle" tip={idleLabel}>
|
|
341
|
+
<motion.button
|
|
280
342
|
type="button"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
343
|
+
data-og-composer-dictate
|
|
344
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
345
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
346
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
347
|
+
transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
|
|
348
|
+
onClick={start}
|
|
349
|
+
aria-label={idleLabel}
|
|
350
|
+
aria-pressed={false}
|
|
351
|
+
aria-disabled={unavailableMessage !== null}
|
|
284
352
|
className={cn(
|
|
285
|
-
"inline-flex size-
|
|
353
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
286
354
|
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
<XIcon className="size-3.5" />
|
|
291
|
-
</button>
|
|
292
|
-
</Tip>
|
|
293
|
-
<Tip tip={messages.stop}>
|
|
294
|
-
<button
|
|
295
|
-
type="button"
|
|
296
|
-
onClick={() => transcription.stop()}
|
|
297
|
-
aria-label={messages.stop}
|
|
298
|
-
className={cn(
|
|
299
|
-
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
300
|
-
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
301
|
-
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
355
|
+
unavailableMessage
|
|
356
|
+
? "cursor-not-allowed opacity-45"
|
|
357
|
+
: "hover:bg-og-surface-2 hover:text-og-fg",
|
|
302
358
|
)}
|
|
303
359
|
>
|
|
304
|
-
<
|
|
305
|
-
</button>
|
|
360
|
+
<MicIcon className="size-4" />
|
|
361
|
+
</motion.button>
|
|
306
362
|
</Tip>
|
|
307
|
-
</>
|
|
308
|
-
) : status === "transcribing" || status === "saving" ? (
|
|
309
|
-
<span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
|
|
310
|
-
{status === "saving" ? messages.saving : messages.transcribing}
|
|
311
|
-
</span>
|
|
312
|
-
) : (
|
|
313
|
-
<span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
|
|
314
|
-
{messages.requestingPermission}
|
|
315
|
-
</span>
|
|
316
|
-
)}
|
|
317
|
-
</motion.span>
|
|
318
|
-
) : (
|
|
319
|
-
<Tip key="idle" tip={idleLabel}>
|
|
320
|
-
<motion.button
|
|
321
|
-
type="button"
|
|
322
|
-
initial={{ opacity: 0, scale: 0.96 }}
|
|
323
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
324
|
-
exit={{ opacity: 0, scale: 0.96 }}
|
|
325
|
-
transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
|
|
326
|
-
onClick={start}
|
|
327
|
-
aria-label={idleLabel}
|
|
328
|
-
aria-pressed={false}
|
|
329
|
-
aria-disabled={unavailableMessage !== null}
|
|
330
|
-
className={cn(
|
|
331
|
-
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
332
|
-
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
333
|
-
unavailableMessage
|
|
334
|
-
? "cursor-not-allowed opacity-45"
|
|
335
|
-
: "hover:bg-og-surface-2 hover:text-og-fg",
|
|
336
363
|
)}
|
|
364
|
+
</AnimatePresence>
|
|
365
|
+
{status === "error" && errorMessage && !recoverable ? (
|
|
366
|
+
<Tip tip={errorMessage}>
|
|
367
|
+
<span
|
|
368
|
+
aria-hidden="true"
|
|
369
|
+
className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
|
|
370
|
+
>
|
|
371
|
+
{errorMessage}
|
|
372
|
+
</span>
|
|
373
|
+
</Tip>
|
|
374
|
+
) : null}
|
|
375
|
+
<span
|
|
376
|
+
className="sr-only"
|
|
377
|
+
role={status === "error" ? "alert" : "status"}
|
|
378
|
+
aria-live="polite"
|
|
337
379
|
>
|
|
338
|
-
|
|
339
|
-
</
|
|
340
|
-
</Tip>
|
|
341
|
-
)}
|
|
342
|
-
</AnimatePresence>
|
|
343
|
-
{status === "error" && errorMessage && !recoverable ? (
|
|
344
|
-
<Tip tip={errorMessage}>
|
|
345
|
-
<span
|
|
346
|
-
aria-hidden="true"
|
|
347
|
-
className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
|
|
348
|
-
>
|
|
349
|
-
{errorMessage}
|
|
380
|
+
{announcement}
|
|
381
|
+
</span>
|
|
350
382
|
</span>
|
|
351
|
-
</
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
{announcement}
|
|
355
|
-
</span>
|
|
356
|
-
</span>
|
|
383
|
+
</motion.span>
|
|
384
|
+
)}
|
|
385
|
+
</AnimatePresence>
|
|
357
386
|
);
|
|
358
387
|
}
|
|
359
388
|
|
|
@@ -241,25 +241,84 @@ export type UseChatComposerControllerOptions = {
|
|
|
241
241
|
};
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* tip-follow
|
|
244
|
+
* Off-document mirror for shrink/steady measure. Measuring with
|
|
245
|
+
* `height: auto` on the live `rows={1}` textarea collapses it for a layout
|
|
246
|
+
* frame, expands the flex timeline sibling, and yanks tip-follow — the
|
|
247
|
+
* multi-line typing flicker that stops only once the box is capped.
|
|
248
|
+
*/
|
|
249
|
+
let composerHeightMirror: HTMLTextAreaElement | null = null;
|
|
250
|
+
|
|
251
|
+
function measureComposerContentHeight(textarea: HTMLTextAreaElement): number {
|
|
252
|
+
if (typeof document === "undefined") {
|
|
253
|
+
return textarea.scrollHeight;
|
|
254
|
+
}
|
|
255
|
+
const width = textarea.clientWidth;
|
|
256
|
+
if (width <= 0) {
|
|
257
|
+
return textarea.offsetHeight;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let mirror = composerHeightMirror;
|
|
261
|
+
if (!mirror) {
|
|
262
|
+
mirror = document.createElement("textarea");
|
|
263
|
+
mirror.setAttribute("aria-hidden", "true");
|
|
264
|
+
mirror.tabIndex = -1;
|
|
265
|
+
mirror.rows = 1;
|
|
266
|
+
mirror.style.cssText =
|
|
267
|
+
"position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;height:auto;min-height:0;max-height:none;overflow:hidden;z-index:-1;";
|
|
268
|
+
composerHeightMirror = mirror;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const style = getComputedStyle(textarea);
|
|
272
|
+
mirror.style.width = `${width}px`;
|
|
273
|
+
mirror.style.boxSizing = style.boxSizing;
|
|
274
|
+
mirror.style.font = style.font;
|
|
275
|
+
mirror.style.fontSize = style.fontSize;
|
|
276
|
+
mirror.style.fontFamily = style.fontFamily;
|
|
277
|
+
mirror.style.fontWeight = style.fontWeight;
|
|
278
|
+
mirror.style.fontStyle = style.fontStyle;
|
|
279
|
+
mirror.style.letterSpacing = style.letterSpacing;
|
|
280
|
+
mirror.style.lineHeight = style.lineHeight;
|
|
281
|
+
mirror.style.textTransform = style.textTransform;
|
|
282
|
+
mirror.style.paddingTop = style.paddingTop;
|
|
283
|
+
mirror.style.paddingRight = style.paddingRight;
|
|
284
|
+
mirror.style.paddingBottom = style.paddingBottom;
|
|
285
|
+
mirror.style.paddingLeft = style.paddingLeft;
|
|
286
|
+
mirror.style.borderTopWidth = style.borderTopWidth;
|
|
287
|
+
mirror.style.borderRightWidth = style.borderRightWidth;
|
|
288
|
+
mirror.style.borderBottomWidth = style.borderBottomWidth;
|
|
289
|
+
mirror.style.borderLeftWidth = style.borderLeftWidth;
|
|
290
|
+
mirror.style.borderStyle = style.borderStyle;
|
|
291
|
+
mirror.style.whiteSpace = style.whiteSpace;
|
|
292
|
+
mirror.style.wordBreak = style.wordBreak;
|
|
293
|
+
mirror.style.overflowWrap = style.overflowWrap;
|
|
294
|
+
mirror.value = textarea.value;
|
|
295
|
+
|
|
296
|
+
if (!mirror.isConnected) {
|
|
297
|
+
document.documentElement.appendChild(mirror);
|
|
298
|
+
}
|
|
299
|
+
return mirror.scrollHeight;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Autosize a composer textarea up to `maxPx`. Never writes intermediate
|
|
304
|
+
* `height: auto` / `0` on the live element — only the final pixel height.
|
|
305
|
+
*
|
|
306
|
+
* `measure` is injectable for unit tests; production always uses the off-DOM
|
|
307
|
+
* mirror so shrink/steady never collapses the laid-out box.
|
|
247
308
|
*/
|
|
248
309
|
export function applyComposerTextareaHeight(
|
|
249
310
|
textarea: HTMLTextAreaElement,
|
|
250
311
|
maxPx: number = 220,
|
|
312
|
+
measure: (el: HTMLTextAreaElement) => number = measureComposerContentHeight,
|
|
251
313
|
): void {
|
|
252
|
-
// Grow path: content already overflows — raise height, no measure collapse.
|
|
253
|
-
if (textarea.scrollHeight > textarea.clientHeight + 1) {
|
|
254
|
-
textarea.style.height = `${Math.min(textarea.scrollHeight, maxPx)}px`;
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
// Shrink / steady: `auto` measures natural height without flashing empty.
|
|
258
314
|
const before = textarea.offsetHeight;
|
|
259
|
-
|
|
260
|
-
|
|
315
|
+
// Overflow: content taller than the box — scrollHeight is the needed size.
|
|
316
|
+
// Fit/shrink: content fits (or box is oversized) — measure off-DOM.
|
|
317
|
+
const nextPx = Math.min(
|
|
318
|
+
textarea.scrollHeight > textarea.clientHeight + 1 ? textarea.scrollHeight : measure(textarea),
|
|
319
|
+
maxPx,
|
|
320
|
+
);
|
|
261
321
|
if (Math.abs(nextPx - before) < 1) {
|
|
262
|
-
textarea.style.height = `${before}px`;
|
|
263
322
|
return;
|
|
264
323
|
}
|
|
265
324
|
textarea.style.height = `${nextPx}px`;
|
|
@@ -934,7 +993,8 @@ export const Footer = forwardRef<HTMLDivElement, ComposerFooterProps>(function C
|
|
|
934
993
|
ref={ref}
|
|
935
994
|
className={cn(
|
|
936
995
|
"flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5",
|
|
937
|
-
|
|
996
|
+
// Mobile: one control row — never wrap into a second toolbar line.
|
|
997
|
+
"max-sm:flex-nowrap max-sm:items-center max-sm:gap-1",
|
|
938
998
|
className,
|
|
939
999
|
)}
|
|
940
1000
|
/>
|
|
@@ -987,7 +1047,11 @@ export const Actions = forwardRef<HTMLSpanElement, ComposerActionsProps>(functio
|
|
|
987
1047
|
<span
|
|
988
1048
|
{...props}
|
|
989
1049
|
ref={ref}
|
|
990
|
-
className={cn(
|
|
1050
|
+
className={cn(
|
|
1051
|
+
"ml-auto flex shrink-0 items-center gap-1.5",
|
|
1052
|
+
"max-sm:flex-nowrap max-sm:gap-1",
|
|
1053
|
+
className,
|
|
1054
|
+
)}
|
|
991
1055
|
/>
|
|
992
1056
|
);
|
|
993
1057
|
});
|
|
@@ -545,7 +545,7 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
|
|
|
545
545
|
disabled={props.disabled}
|
|
546
546
|
aria-label={messages.label}
|
|
547
547
|
className={cn(
|
|
548
|
-
"inline-flex h-8 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-xs text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-7 max-sm:max-w-[
|
|
548
|
+
"inline-flex h-8 min-w-0 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-xs text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-7 max-sm:max-w-[7.5rem] max-sm:px-2",
|
|
549
549
|
props.className,
|
|
550
550
|
)}
|
|
551
551
|
>
|
|
@@ -556,7 +556,12 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
|
|
|
556
556
|
}
|
|
557
557
|
className="text-og-fg"
|
|
558
558
|
/>
|
|
559
|
-
<span className="truncate font-medium text-og-fg">
|
|
559
|
+
<span className="min-w-0 truncate font-medium text-og-fg max-sm:hidden">
|
|
560
|
+
{selected?.label ?? props.model}
|
|
561
|
+
</span>
|
|
562
|
+
<span className="min-w-0 truncate font-medium text-og-fg sm:hidden">
|
|
563
|
+
{selected?.shortLabel ?? selected?.label ?? props.model}
|
|
564
|
+
</span>
|
|
560
565
|
<span className="max-sm:hidden">{labelReasoningEffort(props.effort)}</span>
|
|
561
566
|
{props.latencyMode === "fast" ? (
|
|
562
567
|
<ZapIcon
|