@opengeni/react 1.4.0 → 1.4.4
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 +8 -0
- package/dist/accounts.js +2 -0
- package/dist/accounts.js.map +1 -1
- package/dist/artifacts-document.js +5 -4
- package/dist/artifacts-presentation.js +5 -4
- package/dist/artifacts-spreadsheet.js +4 -3
- package/dist/artifacts.js +14 -13
- package/dist/artifacts.js.map +1 -1
- package/dist/browser-viewer-5EEAS3IU.js +17 -0
- package/dist/chat-interactive-block-ZFHOKTMS.js +12 -0
- package/dist/{chunk-WEHMPOXK.js → chunk-2DDJ6YH6.js} +3 -1
- package/dist/{chunk-WEHMPOXK.js.map → chunk-2DDJ6YH6.js.map} +1 -1
- package/dist/{chunk-C5X5AJXK.js → chunk-2HVLQ26H.js} +5 -4
- package/dist/{chunk-C5X5AJXK.js.map → chunk-2HVLQ26H.js.map} +1 -1
- package/dist/{chunk-HDJWDK7W.js → chunk-2OQA3CPB.js} +300 -166
- package/dist/{chunk-HDJWDK7W.js.map → chunk-2OQA3CPB.js.map} +1 -1
- package/dist/{chunk-A4UJB2MM.js → chunk-32CGLXW2.js} +4 -3
- package/dist/{chunk-A4UJB2MM.js.map → chunk-32CGLXW2.js.map} +1 -1
- package/dist/{chunk-5CVXZA3Y.js → chunk-45NUOSXQ.js} +3 -2
- package/dist/{chunk-5CVXZA3Y.js.map → chunk-45NUOSXQ.js.map} +1 -1
- package/dist/{chunk-WUISZ3WU.js → chunk-53KJBGPZ.js} +5 -4
- package/dist/{chunk-WUISZ3WU.js.map → chunk-53KJBGPZ.js.map} +1 -1
- package/dist/{chunk-MBMPHOMA.js → chunk-55DXD22O.js} +3 -2
- package/dist/{chunk-MBMPHOMA.js.map → chunk-55DXD22O.js.map} +1 -1
- package/dist/{chunk-JT4JW7BC.js → chunk-5I4VRWBB.js} +3 -1
- package/dist/{chunk-JT4JW7BC.js.map → chunk-5I4VRWBB.js.map} +1 -1
- package/dist/{chunk-UK4TINKT.js → chunk-B27E3V37.js} +3 -1
- package/dist/{chunk-UK4TINKT.js.map → chunk-B27E3V37.js.map} +1 -1
- package/dist/{chunk-J6DBS4T3.js → chunk-BNKV5ZLF.js} +5 -4
- package/dist/{chunk-J6DBS4T3.js.map → chunk-BNKV5ZLF.js.map} +1 -1
- package/dist/{chunk-YM2DE7BQ.js → chunk-C5BF4L26.js} +13 -12
- package/dist/{chunk-YM2DE7BQ.js.map → chunk-C5BF4L26.js.map} +1 -1
- package/dist/{chunk-YBXQDQFX.js → chunk-C7CMD56T.js} +6 -5
- package/dist/{chunk-YBXQDQFX.js.map → chunk-C7CMD56T.js.map} +1 -1
- package/dist/{chunk-UGLEYEMP.js → chunk-CM7LVUKC.js} +3 -1
- package/dist/{chunk-UGLEYEMP.js.map → chunk-CM7LVUKC.js.map} +1 -1
- package/dist/{chunk-JVYQRRC4.js → chunk-CVYCLFMH.js} +3 -1
- package/dist/{chunk-JVYQRRC4.js.map → chunk-CVYCLFMH.js.map} +1 -1
- package/dist/{chunk-LJ2UTJJE.js → chunk-EAOSEIXB.js} +182 -97
- package/dist/chunk-EAOSEIXB.js.map +1 -0
- package/dist/{chunk-Y2PRMYLC.js → chunk-G3U2A6GS.js} +3 -2
- package/dist/{chunk-Y2PRMYLC.js.map → chunk-G3U2A6GS.js.map} +1 -1
- package/dist/{chunk-WZMYFJD3.js → chunk-G6YRCIYJ.js} +16 -7
- package/dist/{chunk-WZMYFJD3.js.map → chunk-G6YRCIYJ.js.map} +1 -1
- package/dist/{chunk-B3V6TG77.js → chunk-GC3PH6N7.js} +3 -1
- package/dist/{chunk-B3V6TG77.js.map → chunk-GC3PH6N7.js.map} +1 -1
- package/dist/{chunk-CJUO7J5Z.js → chunk-HXYOSSQO.js} +3 -2
- package/dist/{chunk-CJUO7J5Z.js.map → chunk-HXYOSSQO.js.map} +1 -1
- package/dist/{chunk-GYVRFZDY.js → chunk-I6OY3MWC.js} +310 -115
- package/dist/chunk-I6OY3MWC.js.map +1 -0
- package/dist/{chunk-BRM6FVZA.js → chunk-IEE7WCLN.js} +4 -3
- package/dist/{chunk-BRM6FVZA.js.map → chunk-IEE7WCLN.js.map} +1 -1
- package/dist/{chunk-DYWBUX5Q.js → chunk-IMSUVYQB.js} +6 -5
- package/dist/{chunk-DYWBUX5Q.js.map → chunk-IMSUVYQB.js.map} +1 -1
- package/dist/{chunk-LN6TCXD3.js → chunk-IUHHPMAX.js} +184 -48
- package/dist/chunk-IUHHPMAX.js.map +1 -0
- package/dist/{chunk-SGKD2OCN.js → chunk-IYQCWGU5.js} +6 -5
- package/dist/{chunk-SGKD2OCN.js.map → chunk-IYQCWGU5.js.map} +1 -1
- package/dist/{chunk-J2QWVSQY.js → chunk-JH7V627O.js} +8 -7
- package/dist/{chunk-J2QWVSQY.js.map → chunk-JH7V627O.js.map} +1 -1
- package/dist/{chunk-EESD3PY3.js → chunk-JHTIMBOL.js} +6 -5
- package/dist/{chunk-EESD3PY3.js.map → chunk-JHTIMBOL.js.map} +1 -1
- package/dist/{chunk-LNIRAOA3.js → chunk-L2J3PU7A.js} +7 -6
- package/dist/{chunk-LNIRAOA3.js.map → chunk-L2J3PU7A.js.map} +1 -1
- package/dist/{chunk-ZYRGEKWS.js → chunk-LJUMYUBO.js} +3 -1
- package/dist/{chunk-ZYRGEKWS.js.map → chunk-LJUMYUBO.js.map} +1 -1
- package/dist/{chunk-4RVYOY4O.js → chunk-LSW2OJVV.js} +4 -3
- package/dist/{chunk-4RVYOY4O.js.map → chunk-LSW2OJVV.js.map} +1 -1
- package/dist/{chunk-UTRTG6LH.js → chunk-LVS3MH5T.js} +3 -1
- package/dist/{chunk-UTRTG6LH.js.map → chunk-LVS3MH5T.js.map} +1 -1
- package/dist/chunk-M357MHH3.js +2 -0
- package/dist/{chunk-WRHIZYZF.js → chunk-MEQM4LI3.js} +6 -5
- package/dist/{chunk-WRHIZYZF.js.map → chunk-MEQM4LI3.js.map} +1 -1
- package/dist/chunk-NX4ADH73.js +14 -0
- package/dist/chunk-NX4ADH73.js.map +1 -0
- package/dist/{chunk-XCHWDJ22.js → chunk-OGTTUYNT.js} +3 -1
- package/dist/{chunk-XCHWDJ22.js.map → chunk-OGTTUYNT.js.map} +1 -1
- package/dist/{chunk-GIMRELBW.js → chunk-OYQQJL3J.js} +3 -1
- package/dist/{chunk-GIMRELBW.js.map → chunk-OYQQJL3J.js.map} +1 -1
- package/dist/{chunk-RSI7XYXL.js → chunk-PBCZL4JP.js} +6 -5
- package/dist/{chunk-RSI7XYXL.js.map → chunk-PBCZL4JP.js.map} +1 -1
- package/dist/{chunk-I7IWRQLN.js → chunk-PHBFJCEU.js} +17 -11
- package/dist/chunk-PHBFJCEU.js.map +1 -0
- package/dist/{chunk-3CTRFMAK.js → chunk-PKCAYHDC.js} +5 -4
- package/dist/{chunk-3CTRFMAK.js.map → chunk-PKCAYHDC.js.map} +1 -1
- package/dist/{chunk-K3HQAKQX.js → chunk-PMV53IMM.js} +3 -1
- package/dist/{chunk-K3HQAKQX.js.map → chunk-PMV53IMM.js.map} +1 -1
- package/dist/{chunk-YQEAA6LT.js → chunk-QHUE7E4N.js} +3 -2
- package/dist/{chunk-YQEAA6LT.js.map → chunk-QHUE7E4N.js.map} +1 -1
- package/dist/{chunk-OA6HFRQP.js → chunk-QMMCJGFI.js} +4 -3
- package/dist/{chunk-OA6HFRQP.js.map → chunk-QMMCJGFI.js.map} +1 -1
- package/dist/{chunk-YV4L3ILF.js → chunk-QV3DJVRZ.js} +3 -1
- package/dist/{chunk-YV4L3ILF.js.map → chunk-QV3DJVRZ.js.map} +1 -1
- package/dist/{chunk-447JLCBU.js → chunk-S2QH6EZ5.js} +3 -1
- package/dist/{chunk-447JLCBU.js.map → chunk-S2QH6EZ5.js.map} +1 -1
- package/dist/{chunk-PNY3JKBJ.js → chunk-SFH43JT2.js} +5 -4
- package/dist/{chunk-PNY3JKBJ.js.map → chunk-SFH43JT2.js.map} +1 -1
- package/dist/{chunk-AWZLIMKX.js → chunk-T46QZH3E.js} +3 -1
- package/dist/{chunk-AWZLIMKX.js.map → chunk-T46QZH3E.js.map} +1 -1
- package/dist/{chunk-5J4FOW6Q.js → chunk-TNEZNMSN.js} +3 -1
- package/dist/{chunk-5J4FOW6Q.js.map → chunk-TNEZNMSN.js.map} +1 -1
- package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
- package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
- package/dist/{chunk-H5X55XH7.js → chunk-UDPH5SLV.js} +4 -3
- package/dist/{chunk-H5X55XH7.js.map → chunk-UDPH5SLV.js.map} +1 -1
- package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
- package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
- package/dist/{chunk-5F4WR7X7.js → chunk-XS67YG5R.js} +3 -1
- package/dist/{chunk-5F4WR7X7.js.map → chunk-XS67YG5R.js.map} +1 -1
- package/dist/{chunk-TKOTKRWY.js → chunk-XTD7TZHW.js} +3 -1
- package/dist/{chunk-TKOTKRWY.js.map → chunk-XTD7TZHW.js.map} +1 -1
- package/dist/{chunk-A2PI6UMY.js → chunk-YSRBFJMQ.js} +3 -1
- package/dist/{chunk-A2PI6UMY.js.map → chunk-YSRBFJMQ.js.map} +1 -1
- package/dist/chunk-YVFRBXST.js +1420 -0
- package/dist/chunk-YVFRBXST.js.map +1 -0
- package/dist/{chunk-SCOTPE2I.js → chunk-YYLBXXTE.js} +3 -1
- package/dist/{chunk-SCOTPE2I.js.map → chunk-YYLBXXTE.js.map} +1 -1
- package/dist/{chunk-RHREZ7BO.js → chunk-ZUWF4NF7.js} +3 -1
- package/dist/{chunk-RHREZ7BO.js.map → chunk-ZUWF4NF7.js.map} +1 -1
- package/dist/clipboard.js +3 -2
- package/dist/components/composer-transcription-control.d.ts +4 -0
- package/dist/components/session-conversation.d.ts +6 -0
- package/dist/composer-voice-events.d.ts +8 -0
- package/dist/composer.js +15 -13
- package/dist/computer-viewer-ELNLZGHI.js +20 -0
- package/dist/connect.js +15 -12
- package/dist/connect.js.map +1 -1
- package/dist/desktop.js +6 -5
- package/dist/desktop.js.map +1 -1
- package/dist/diffs.js +2 -1
- package/dist/diffs.js.map +1 -1
- package/dist/editor.js +4 -3
- package/dist/editor.js.map +1 -1
- package/dist/embedded-voice-NIL4RDX3.js +69 -0
- package/dist/embedded-voice-NIL4RDX3.js.map +1 -0
- package/dist/{fleet-decision-row-YZ3ZKWMM.js → fleet-decision-row-SI63O5V4.js} +3 -1
- package/dist/{fleet-decision-row-YZ3ZKWMM.js.map → fleet-decision-row-SI63O5V4.js.map} +1 -1
- package/dist/hooks/use-client-config-flags.d.ts +6 -0
- package/dist/hooks/use-computer-session.d.ts +2 -0
- package/dist/hooks/use-realtime-voice-models.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +8 -1
- package/dist/index.js +47 -45
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +14 -13
- package/dist/interaction.js.map +1 -1
- package/dist/{latest-question-GPYM2JNU.js → latest-question-2POFPYNB.js} +5 -4
- package/dist/{latest-question-GPYM2JNU.js.map → latest-question-2POFPYNB.js.map} +1 -1
- package/dist/machines.js +8 -7
- package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-DQ4DRNVT.js} +3 -1
- package/dist/{markdown-table-layout-QXSLK4BY.js.map → markdown-table-layout-DQ4DRNVT.js.map} +1 -1
- package/dist/model-policy-picker-menu-WRNSRFQC.js +15 -0
- package/dist/model-policy.js +2 -1
- package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
- package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
- package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
- package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
- package/dist/{platform-activity-row-627BFZ46.js → platform-activity-row-PJJVATTZ.js} +3 -1
- package/dist/{platform-activity-row-627BFZ46.js.map → platform-activity-row-PJJVATTZ.js.map} +1 -1
- package/dist/{queue-surface-implementation-ZHFI5JBP.js → queue-surface-implementation-67FS7FUI.js} +10 -9
- package/dist/{queue-surface-implementation-ZHFI5JBP.js.map → queue-surface-implementation-67FS7FUI.js.map} +1 -1
- package/dist/realtime/embedded-voice.d.ts +20 -0
- package/dist/realtime.js +22 -1353
- package/dist/realtime.js.map +1 -1
- package/dist/sandbox-files-34CJJNOY.js +21 -0
- package/dist/session-ui.js +32 -30
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +10 -9
- package/dist/sites.js +2 -1
- package/dist/terminal.js +6 -5
- package/dist/terminal.js.map +1 -1
- package/dist/{timeline-annotation-markers-GNDHTGBD.js → timeline-annotation-markers-U3G66KUD.js} +5 -4
- package/dist/{timeline-annotation-markers-GNDHTGBD.js.map → timeline-annotation-markers-U3G66KUD.js.map} +1 -1
- package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-BI7QTPTZ.js} +4 -3
- package/dist/{timeline-annotation-selection-DMLMM2TX.js.map → timeline-annotation-selection-BI7QTPTZ.js.map} +1 -1
- package/dist/{timeline-annotations-dialog-ELRDBHTS.js → timeline-annotations-dialog-FOMHSDTE.js} +7 -6
- package/dist/{timeline-annotations-dialog-ELRDBHTS.js.map → timeline-annotations-dialog-FOMHSDTE.js.map} +1 -1
- package/dist/usage.js +6 -5
- package/dist/usage.js.map +1 -1
- package/dist/voice-recording-owner.d.ts +7 -0
- package/package.json +3 -3
- package/src/components/composer-transcription-control.tsx +109 -27
- package/src/components/computer-viewer.tsx +139 -30
- package/src/components/markdown.tsx +7 -2
- package/src/components/message-timeline.tsx +10 -1
- package/src/components/session-conversation.tsx +44 -0
- package/src/components/session-list.tsx +1 -1
- package/src/components/session-proxy-scope.tsx +26 -5
- package/src/composer-voice-events.ts +12 -0
- package/src/connection-discovery.tsx +4 -1
- package/src/hooks/use-browser-session.ts +445 -168
- package/src/hooks/use-client-config-flags.ts +8 -0
- package/src/hooks/use-computer-session.ts +334 -98
- package/src/hooks/use-goal.ts +12 -1
- package/src/hooks/use-realtime-voice-models.ts +67 -0
- package/src/hooks/use-voice-input.ts +109 -18
- package/src/realtime/embedded-voice.tsx +81 -0
- package/src/realtime/realtime-control.tsx +82 -17
- package/src/voice-recording-owner.ts +22 -0
- package/styles/compiled.css +77 -18
- package/styles/index.css +49 -0
- package/styles/responsive.css +61 -4
- package/styles/session-capability.css +1 -1
- package/dist/browser-viewer-SHYKQRNX.js +0 -16
- package/dist/chat-interactive-block-UGBTNB3L.js +0 -11
- package/dist/chunk-GYVRFZDY.js.map +0 -1
- package/dist/chunk-I7IWRQLN.js.map +0 -1
- package/dist/chunk-IWIDV7SB.js +0 -1
- package/dist/chunk-LJ2UTJJE.js.map +0 -1
- package/dist/chunk-LN6TCXD3.js.map +0 -1
- package/dist/computer-viewer-6SRGWRUX.js +0 -19
- package/dist/model-policy-picker-menu-DXHUL73H.js +0 -14
- package/dist/sandbox-files-GORZOUUJ.js +0 -20
- /package/dist/{browser-viewer-SHYKQRNX.js.map → browser-viewer-5EEAS3IU.js.map} +0 -0
- /package/dist/{chat-interactive-block-UGBTNB3L.js.map → chat-interactive-block-ZFHOKTMS.js.map} +0 -0
- /package/dist/{chunk-IWIDV7SB.js.map → chunk-M357MHH3.js.map} +0 -0
- /package/dist/{computer-viewer-6SRGWRUX.js.map → computer-viewer-ELNLZGHI.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-DXHUL73H.js.map → model-policy-picker-menu-WRNSRFQC.js.map} +0 -0
- /package/dist/{sandbox-files-GORZOUUJ.js.map → sandbox-files-34CJJNOY.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPOSER_VOICE_INPUT_START_EVENT,
|
|
3
|
+
composerVoiceEventScope,
|
|
4
|
+
} from "../composer-voice-events";
|
|
1
5
|
import type {
|
|
2
6
|
ClientVoiceInputConfig,
|
|
3
7
|
OpenGeniClient,
|
|
@@ -35,6 +39,10 @@ export type ComposerTranscriptionMessages = {
|
|
|
35
39
|
stop: string;
|
|
36
40
|
cancel: string;
|
|
37
41
|
retry: string;
|
|
42
|
+
/** Transcribe a recording that was stopped without transcribing or recovered. */
|
|
43
|
+
transcribeSaved: string;
|
|
44
|
+
/** Shown near the automatic stop; `{seconds}` is replaced. */
|
|
45
|
+
timeRemaining: string;
|
|
38
46
|
pauseRetrying: string;
|
|
39
47
|
requestingPermission: string;
|
|
40
48
|
recording: string;
|
|
@@ -64,32 +72,34 @@ export type ComposerTranscriptionMessages = {
|
|
|
64
72
|
const defaultMessages: ComposerTranscriptionMessages = {
|
|
65
73
|
start: "Start voice input",
|
|
66
74
|
stop: "Stop and transcribe",
|
|
67
|
-
cancel: "
|
|
75
|
+
cancel: "Stop without transcribing",
|
|
68
76
|
retry: "Retry voice input",
|
|
77
|
+
transcribeSaved: "Transcribe saved recording",
|
|
78
|
+
timeRemaining: "Stops and transcribes in {seconds}s",
|
|
69
79
|
pauseRetrying: "Pause automatic retry",
|
|
70
80
|
requestingPermission: "Requesting microphone…",
|
|
71
|
-
recording: "Recording. Press Escape to
|
|
81
|
+
recording: "Recording. Press Escape to stop without transcribing.",
|
|
72
82
|
saving: "Saving audio locally…",
|
|
73
83
|
transcribing: "Transcribing…",
|
|
74
|
-
retrying: "
|
|
75
|
-
recovered: "Recording
|
|
76
|
-
recoveredTranscript: "Transcript saved
|
|
84
|
+
retrying: "Saved. Retrying transcription…",
|
|
85
|
+
recovered: "Recording saved on this device.",
|
|
86
|
+
recoveredTranscript: "Transcript saved. Insert it into your draft?",
|
|
77
87
|
insertRecoveredTranscript: "Insert saved transcript",
|
|
78
88
|
discardRecovered: "Discard saved recording",
|
|
79
89
|
unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
|
|
80
90
|
unavailable: "Voice input is unavailable for this workspace.",
|
|
81
|
-
errorPermissionDenied: "Microphone
|
|
91
|
+
errorPermissionDenied: "Microphone access is blocked. Allow it in site settings, then try again.",
|
|
82
92
|
errorNotSupported: "Voice input is not supported on this device.",
|
|
83
93
|
errorUnavailable: "Voice input is not configured.",
|
|
84
94
|
errorInsufficientCredits:
|
|
85
|
-
"Voice input needs
|
|
86
|
-
errorAllowanceExhausted: "
|
|
95
|
+
"Voice input needs credits. Recording saved; retry after adding credits.",
|
|
96
|
+
errorAllowanceExhausted: "Usage limit reached. Recording saved for later.",
|
|
87
97
|
errorPolicyBlocked: "Voice input isn't allowed for this account or workspace.",
|
|
88
98
|
errorTooLarge: "Recording is too large. Try a shorter message.",
|
|
89
99
|
errorInvalidAudio: "The recording could not be read. Try again.",
|
|
90
100
|
errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
|
|
91
|
-
errorRetryable: "
|
|
92
|
-
errorHandoffUncertain: "Transcript
|
|
101
|
+
errorRetryable: "Saved. Transcription failed, retry when ready.",
|
|
102
|
+
errorHandoffUncertain: "Transcript saved. Check your draft before inserting again.",
|
|
93
103
|
errorUnknown: "Voice input could not start. Try again.",
|
|
94
104
|
};
|
|
95
105
|
|
|
@@ -153,12 +163,18 @@ export function ComposerTranscriptionControl({
|
|
|
153
163
|
const capturing = status === "requesting-permission" || status === "recording";
|
|
154
164
|
|
|
155
165
|
const cancelTranscription = transcription.cancel;
|
|
166
|
+
const discardTranscription = transcription.discard;
|
|
167
|
+
const hasRecoverableRecording = transcription.hasRecoverableRecording;
|
|
156
168
|
useEffect(() => {
|
|
157
169
|
if (!suppressed) return;
|
|
158
170
|
if (status === "recording" || status === "requesting-permission") {
|
|
159
171
|
cancelTranscription();
|
|
172
|
+
} else if (status === "error" && !hasRecoverableRecording) {
|
|
173
|
+
// Live voice took over: a stale start error (nothing saved) would only
|
|
174
|
+
// duplicate whatever live voice reports next to it.
|
|
175
|
+
void discardTranscription();
|
|
160
176
|
}
|
|
161
|
-
}, [cancelTranscription, status, suppressed]);
|
|
177
|
+
}, [cancelTranscription, discardTranscription, hasRecoverableRecording, status, suppressed]);
|
|
162
178
|
const active = capturing || status === "saving" || status === "transcribing";
|
|
163
179
|
const recoverable =
|
|
164
180
|
transcription.hasRecoverableRecording &&
|
|
@@ -194,11 +210,22 @@ export function ComposerTranscriptionControl({
|
|
|
194
210
|
? (errorMessage ?? messages.errorUnknown)
|
|
195
211
|
: unavailableMessage;
|
|
196
212
|
|
|
213
|
+
const recoveredLabel = savedTranscript
|
|
214
|
+
? (errorMessage ?? messages.recoveredTranscript)
|
|
215
|
+
: retrying
|
|
216
|
+
? messages.retrying
|
|
217
|
+
: status === "error"
|
|
218
|
+
? (errorMessage ?? messages.errorRetryable)
|
|
219
|
+
: messages.recovered;
|
|
220
|
+
|
|
197
221
|
function start(event: MouseEvent<HTMLButtonElement>) {
|
|
198
222
|
if (unavailableMessage) {
|
|
199
223
|
event.preventDefault();
|
|
200
224
|
return;
|
|
201
225
|
}
|
|
226
|
+
composerVoiceEventScope(event.currentTarget)?.dispatchEvent(
|
|
227
|
+
new Event(COMPOSER_VOICE_INPUT_START_EVENT),
|
|
228
|
+
);
|
|
202
229
|
void transcription.start();
|
|
203
230
|
}
|
|
204
231
|
|
|
@@ -220,6 +247,9 @@ export function ComposerTranscriptionControl({
|
|
|
220
247
|
)}
|
|
221
248
|
data-transcription-status={status}
|
|
222
249
|
data-transcription-capturing={capturing ? "" : undefined}
|
|
250
|
+
data-transcription-attention={
|
|
251
|
+
!capturing && (recoverable || (status === "error" && errorMessage)) ? "" : undefined
|
|
252
|
+
}
|
|
223
253
|
>
|
|
224
254
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
225
255
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
@@ -235,14 +265,11 @@ export function ComposerTranscriptionControl({
|
|
|
235
265
|
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
236
266
|
)}
|
|
237
267
|
>
|
|
238
|
-
<span
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: status === "error"
|
|
244
|
-
? (errorMessage ?? messages.errorRetryable)
|
|
245
|
-
: messages.recovered}
|
|
268
|
+
<span
|
|
269
|
+
title={recoveredLabel}
|
|
270
|
+
className="og-transcription-recovered-label max-w-56 line-clamp-2 text-og-xs leading-tight text-og-fg-muted max-sm:max-w-32"
|
|
271
|
+
>
|
|
272
|
+
{recoveredLabel}
|
|
246
273
|
</span>
|
|
247
274
|
<Tip
|
|
248
275
|
tip={
|
|
@@ -250,7 +277,9 @@ export function ComposerTranscriptionControl({
|
|
|
250
277
|
? messages.insertRecoveredTranscript
|
|
251
278
|
: retrying
|
|
252
279
|
? messages.pauseRetrying
|
|
253
|
-
:
|
|
280
|
+
: status === "recovered"
|
|
281
|
+
? messages.transcribeSaved
|
|
282
|
+
: messages.retry
|
|
254
283
|
}
|
|
255
284
|
>
|
|
256
285
|
<button
|
|
@@ -268,7 +297,9 @@ export function ComposerTranscriptionControl({
|
|
|
268
297
|
? messages.insertRecoveredTranscript
|
|
269
298
|
: retrying
|
|
270
299
|
? messages.pauseRetrying
|
|
271
|
-
:
|
|
300
|
+
: status === "recovered"
|
|
301
|
+
? messages.transcribeSaved
|
|
302
|
+
: messages.retry
|
|
272
303
|
}
|
|
273
304
|
className={cn(
|
|
274
305
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
@@ -330,6 +361,13 @@ export function ComposerTranscriptionControl({
|
|
|
330
361
|
stream={status === "recording" ? transcription.stream : null}
|
|
331
362
|
mode={status === "recording" ? "recording" : "transcribing"}
|
|
332
363
|
/>
|
|
364
|
+
{status === "recording" && transcription.recordingStartedAt !== null ? (
|
|
365
|
+
<RecordingClock
|
|
366
|
+
startedAt={transcription.recordingStartedAt}
|
|
367
|
+
maxSeconds={transcription.maxRecordingSeconds}
|
|
368
|
+
remainingLabel={messages.timeRemaining}
|
|
369
|
+
/>
|
|
370
|
+
) : null}
|
|
333
371
|
</span>
|
|
334
372
|
)}
|
|
335
373
|
{status === "recording" ? (
|
|
@@ -401,14 +439,22 @@ export function ComposerTranscriptionControl({
|
|
|
401
439
|
)}
|
|
402
440
|
</AnimatePresence>
|
|
403
441
|
{status === "error" && errorMessage && !recoverable ? (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
442
|
+
// Nothing is saved in this state, so dismissing loses no speech.
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
title={errorMessage}
|
|
446
|
+
aria-label={`Dismiss: ${errorMessage}`}
|
|
447
|
+
onClick={() => void transcription.discard()}
|
|
448
|
+
className={cn(
|
|
449
|
+
"inline-flex min-w-0 items-center gap-1 rounded-og-sm text-left outline-hidden",
|
|
450
|
+
"text-og-status-failed focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
451
|
+
)}
|
|
452
|
+
>
|
|
453
|
+
<span className="og-transcription-error-label max-w-56 line-clamp-2 text-og-xs leading-tight max-sm:max-w-32">
|
|
409
454
|
{errorMessage}
|
|
410
455
|
</span>
|
|
411
|
-
|
|
456
|
+
<XIcon className="size-3 shrink-0 opacity-70" aria-hidden />
|
|
457
|
+
</button>
|
|
412
458
|
) : null}
|
|
413
459
|
<span
|
|
414
460
|
className="sr-only"
|
|
@@ -424,6 +470,42 @@ export function ComposerTranscriptionControl({
|
|
|
424
470
|
);
|
|
425
471
|
}
|
|
426
472
|
|
|
473
|
+
/** Elapsed dictation time; warns before the automatic stop-and-transcribe. */
|
|
474
|
+
function RecordingClock({
|
|
475
|
+
startedAt,
|
|
476
|
+
maxSeconds,
|
|
477
|
+
remainingLabel,
|
|
478
|
+
}: {
|
|
479
|
+
startedAt: number;
|
|
480
|
+
maxSeconds: number | null;
|
|
481
|
+
remainingLabel: string;
|
|
482
|
+
}) {
|
|
483
|
+
const [now, setNow] = useState(() => Date.now());
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const timer = setInterval(() => setNow(Date.now()), 500);
|
|
486
|
+
return () => clearInterval(timer);
|
|
487
|
+
}, []);
|
|
488
|
+
const elapsed = Math.max(0, Math.floor((now - startedAt) / 1_000));
|
|
489
|
+
const remaining = maxSeconds === null ? null : Math.max(0, maxSeconds - elapsed);
|
|
490
|
+
const warn = remaining !== null && remaining <= 15;
|
|
491
|
+
const label = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, "0")}`;
|
|
492
|
+
return (
|
|
493
|
+
<span
|
|
494
|
+
data-recording-clock
|
|
495
|
+
title={warn ? remainingLabel.replace("{seconds}", String(remaining)) : undefined}
|
|
496
|
+
className={cn(
|
|
497
|
+
"min-w-[2.25rem] text-right text-og-xs tabular-nums",
|
|
498
|
+
warn ? "font-medium text-og-status-failed" : "text-og-fg-muted",
|
|
499
|
+
)}
|
|
500
|
+
>
|
|
501
|
+
{label}
|
|
502
|
+
{warn ? (
|
|
503
|
+
<span className="sr-only">{remainingLabel.replace("{seconds}", String(remaining))}</span>
|
|
504
|
+
) : null}
|
|
505
|
+
</span>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
427
509
|
function VoiceWaveform({
|
|
428
510
|
stream,
|
|
429
511
|
mode,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ComputerAction,
|
|
3
|
+
ComputerActionReceipt,
|
|
3
4
|
ComputerClipboard,
|
|
4
5
|
ComputerFrame,
|
|
5
6
|
ComputerObservation,
|
|
@@ -42,7 +43,11 @@ import {
|
|
|
42
43
|
type ComputerFrameWebSocketFactory,
|
|
43
44
|
useComputerFrameStream,
|
|
44
45
|
} from "../hooks/use-computer-frame-stream";
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
computerActionReceiptError,
|
|
48
|
+
isStaleComputerActionResult,
|
|
49
|
+
useComputerSession,
|
|
50
|
+
} from "../hooks/use-computer-session";
|
|
46
51
|
import { useComputerInputPosture } from "../hooks/use-computer-input-posture";
|
|
47
52
|
import { useComputerSessions } from "../hooks/use-computer-sessions";
|
|
48
53
|
import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
|
|
@@ -174,6 +179,7 @@ export function ComputerViewer({
|
|
|
174
179
|
|
|
175
180
|
const notifyError = useCallback(
|
|
176
181
|
(cause: unknown, fallback: string) => {
|
|
182
|
+
if (isStaleComputerActionResult(cause)) return;
|
|
177
183
|
onNotify?.({
|
|
178
184
|
kind: "error",
|
|
179
185
|
message: cause instanceof Error ? cause.message : fallback,
|
|
@@ -490,19 +496,24 @@ export function ComputerViewer({
|
|
|
490
496
|
]);
|
|
491
497
|
|
|
492
498
|
const perform = useCallback(
|
|
493
|
-
async (
|
|
499
|
+
async (
|
|
500
|
+
action: ComputerAction,
|
|
501
|
+
frame: ComputerFrame | null,
|
|
502
|
+
operationId?: string,
|
|
503
|
+
): Promise<ComputerActionReceipt> => {
|
|
494
504
|
if (computer.controlError) throw computer.controlError;
|
|
495
505
|
if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
|
|
496
506
|
let receipt;
|
|
497
507
|
if (action.type === "pointer") {
|
|
498
508
|
if (!frame) throw new Error("Desktop view is not ready for pointer input.");
|
|
499
|
-
receipt = await actFromFrame(action, frame);
|
|
509
|
+
receipt = await actFromFrame(action, frame, operationId);
|
|
500
510
|
} else {
|
|
501
|
-
receipt = await act(action);
|
|
511
|
+
receipt = await act(action, operationId);
|
|
502
512
|
}
|
|
503
|
-
if (receipt.state !== "completed") {
|
|
504
|
-
throw
|
|
513
|
+
if (!isStaleComputerActionResult(receipt) && receipt.state !== "completed") {
|
|
514
|
+
throw computerActionReceiptError(receipt);
|
|
505
515
|
}
|
|
516
|
+
return receipt;
|
|
506
517
|
},
|
|
507
518
|
[act, actFromFrame, computer.controlError, inputDenied],
|
|
508
519
|
);
|
|
@@ -519,14 +530,15 @@ export function ComputerViewer({
|
|
|
519
530
|
return;
|
|
520
531
|
event.preventDefault();
|
|
521
532
|
event.stopPropagation();
|
|
522
|
-
void
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
533
|
+
void (async () => {
|
|
534
|
+
const receipt = await perform({ type: "clipboard", operation: "copy" }, null);
|
|
535
|
+
if (isStaleComputerActionResult(receipt)) return;
|
|
536
|
+
const clipboard = await computer.readClipboard();
|
|
537
|
+
if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
|
|
538
|
+
if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
|
|
539
|
+
throw new Error("Desktop text could not be copied to the local clipboard");
|
|
540
|
+
}
|
|
541
|
+
})().catch((cause) => notifyError(cause, "Could not copy from the desktop."));
|
|
530
542
|
},
|
|
531
543
|
[computer, notifyError, perform, rfbInputEnabled, rfbStream],
|
|
532
544
|
);
|
|
@@ -543,8 +555,11 @@ export function ComputerViewer({
|
|
|
543
555
|
// ownership is asynchronous, so a successful write response alone does
|
|
544
556
|
// not prove that the graphical seat can already serve the selection.
|
|
545
557
|
void (async () => {
|
|
546
|
-
await perform({ type: "clipboard", operation: "write", text }, null);
|
|
547
|
-
|
|
558
|
+
const receipt = await perform({ type: "clipboard", operation: "write", text }, null);
|
|
559
|
+
if (isStaleComputerActionResult(receipt)) return;
|
|
560
|
+
const clipboard = await computer.readClipboard();
|
|
561
|
+
if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
|
|
562
|
+
assertExactComputerClipboard(clipboard, text);
|
|
548
563
|
await perform({ type: "clipboard", operation: "paste" }, null);
|
|
549
564
|
})().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
|
|
550
565
|
return true;
|
|
@@ -750,6 +765,10 @@ export function ComputerViewer({
|
|
|
750
765
|
pointerInput={
|
|
751
766
|
attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true
|
|
752
767
|
}
|
|
768
|
+
pointerClickContinuation={
|
|
769
|
+
attachmentInputAllowed &&
|
|
770
|
+
computer.session?.capabilities?.pointerClickContinuation === true
|
|
771
|
+
}
|
|
753
772
|
keyboardInput={
|
|
754
773
|
attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true
|
|
755
774
|
}
|
|
@@ -1194,6 +1213,12 @@ function ComputerLifecyclePanel(props: {
|
|
|
1194
1213
|
);
|
|
1195
1214
|
}
|
|
1196
1215
|
|
|
1216
|
+
type ComputerQueuedAction = {
|
|
1217
|
+
operationId: string;
|
|
1218
|
+
started: Promise<boolean>;
|
|
1219
|
+
completion: Promise<void>;
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1197
1222
|
function ComputerViewport(props: {
|
|
1198
1223
|
frame: ComputerFrame | null;
|
|
1199
1224
|
observation: ComputerObservation | null;
|
|
@@ -1208,8 +1233,13 @@ function ComputerViewport(props: {
|
|
|
1208
1233
|
backgroundInput: boolean;
|
|
1209
1234
|
clipboardEnabled: boolean;
|
|
1210
1235
|
pointerInput: boolean;
|
|
1236
|
+
pointerClickContinuation: boolean;
|
|
1211
1237
|
keyboardInput: boolean;
|
|
1212
|
-
onAction: (
|
|
1238
|
+
onAction: (
|
|
1239
|
+
action: ComputerAction,
|
|
1240
|
+
frame: ComputerFrame | null,
|
|
1241
|
+
operationId?: string,
|
|
1242
|
+
) => Promise<ComputerActionReceipt>;
|
|
1213
1243
|
onReadClipboard: () => Promise<ComputerClipboard>;
|
|
1214
1244
|
onReconnect: () => void;
|
|
1215
1245
|
onError: (cause: unknown) => void;
|
|
@@ -1224,6 +1254,11 @@ function ComputerViewport(props: {
|
|
|
1224
1254
|
x: number;
|
|
1225
1255
|
y: number;
|
|
1226
1256
|
frame: ComputerFrame;
|
|
1257
|
+
clientX: number;
|
|
1258
|
+
clientY: number;
|
|
1259
|
+
canvasRect: Pick<DOMRect, "left" | "top" | "width" | "height">;
|
|
1260
|
+
pending: boolean;
|
|
1261
|
+
delivery: ComputerQueuedAction | null;
|
|
1227
1262
|
} | null>(null);
|
|
1228
1263
|
const wheelRef = useRef<{
|
|
1229
1264
|
x: number;
|
|
@@ -1394,24 +1429,48 @@ function ComputerViewport(props: {
|
|
|
1394
1429
|
action: ComputerAction,
|
|
1395
1430
|
frame: ComputerFrame | null,
|
|
1396
1431
|
after?: (isCurrent: () => boolean) => Promise<void>,
|
|
1432
|
+
continuation?: ComputerQueuedAction,
|
|
1397
1433
|
) => {
|
|
1398
1434
|
const epoch = actionQueueEpochRef.current;
|
|
1399
1435
|
const dispatch = actionRef.current;
|
|
1400
1436
|
const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1437
|
+
const operationId = crypto.randomUUID();
|
|
1438
|
+
let markStarted!: (value: boolean) => void;
|
|
1439
|
+
const started = new Promise<boolean>((resolve) => {
|
|
1440
|
+
markStarted = resolve;
|
|
1441
|
+
});
|
|
1442
|
+
const previous = actionTailRef.current;
|
|
1443
|
+
// Only the real second click may start before its first HTTP receipt.
|
|
1444
|
+
// The controller queue and native causal proof still serialize delivery.
|
|
1445
|
+
const ready =
|
|
1446
|
+
continuation && previous === continuation.completion
|
|
1447
|
+
? continuation.started
|
|
1448
|
+
: previous.then(() => true);
|
|
1449
|
+
const delivery = ready
|
|
1450
|
+
.then(async (firstStarted) => {
|
|
1451
|
+
if (!firstStarted || !isCurrent()) {
|
|
1452
|
+
markStarted(false);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const pending = dispatch(action, frame, operationId);
|
|
1456
|
+
markStarted(true);
|
|
1457
|
+
const receipt = await pending;
|
|
1458
|
+
const isCurrentResult = () => isCurrent() && !isStaleComputerActionResult(receipt);
|
|
1459
|
+
if (!isCurrentResult()) return;
|
|
1460
|
+
await after?.(isCurrentResult);
|
|
1408
1461
|
})
|
|
1409
1462
|
.catch((cause) => {
|
|
1410
|
-
|
|
1463
|
+
markStarted(false);
|
|
1464
|
+
if (!isCurrent() || isStaleComputerActionResult(cause)) return;
|
|
1411
1465
|
actionQueueEpochRef.current += 1;
|
|
1412
1466
|
clearBufferedInput();
|
|
1413
1467
|
errorRef.current(cause);
|
|
1414
1468
|
});
|
|
1469
|
+
const completion = continuation
|
|
1470
|
+
? Promise.all([previous, delivery]).then(() => undefined)
|
|
1471
|
+
: delivery;
|
|
1472
|
+
actionTailRef.current = completion;
|
|
1473
|
+
return { operationId, started, completion };
|
|
1415
1474
|
},
|
|
1416
1475
|
[clearBufferedInput],
|
|
1417
1476
|
);
|
|
@@ -1436,6 +1495,7 @@ function ComputerViewport(props: {
|
|
|
1436
1495
|
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
1437
1496
|
clickTimerRef.current = null;
|
|
1438
1497
|
lastClickRef.current = null;
|
|
1498
|
+
if (!pending.pending) return;
|
|
1439
1499
|
enqueue(
|
|
1440
1500
|
{
|
|
1441
1501
|
type: "pointer",
|
|
@@ -1469,7 +1529,13 @@ function ComputerViewport(props: {
|
|
|
1469
1529
|
|
|
1470
1530
|
const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
1471
1531
|
const frame = paintedFrameRef.current;
|
|
1472
|
-
if (
|
|
1532
|
+
if (
|
|
1533
|
+
!frame ||
|
|
1534
|
+
(!props.pointerClickContinuation && props.mutating) ||
|
|
1535
|
+
!rawInputEnabled ||
|
|
1536
|
+
event.button !== 0
|
|
1537
|
+
)
|
|
1538
|
+
return;
|
|
1473
1539
|
// Preserve the keyboard sink's focus through the canvas pointer default action.
|
|
1474
1540
|
event.preventDefault();
|
|
1475
1541
|
if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
|
|
@@ -1490,6 +1556,7 @@ function ComputerViewport(props: {
|
|
|
1490
1556
|
const start = pointerStartRef.current;
|
|
1491
1557
|
pointerStartRef.current = null;
|
|
1492
1558
|
if (!start || start.pointerId !== event.pointerId) return;
|
|
1559
|
+
const canvasRect = event.currentTarget.getBoundingClientRect();
|
|
1493
1560
|
const from = point(start.frame, start.x, start.y);
|
|
1494
1561
|
const to = point(start.frame, event.clientX, event.clientY);
|
|
1495
1562
|
if (!from || !to) return;
|
|
@@ -1514,8 +1581,18 @@ function ComputerViewport(props: {
|
|
|
1514
1581
|
if (
|
|
1515
1582
|
previous &&
|
|
1516
1583
|
now - previous.at < 280 &&
|
|
1517
|
-
|
|
1518
|
-
|
|
1584
|
+
previous.pending === !props.pointerClickContinuation &&
|
|
1585
|
+
(props.pointerClickContinuation
|
|
1586
|
+
? Math.hypot(previous.clientX - event.clientX, previous.clientY - event.clientY) < 6 &&
|
|
1587
|
+
previous.canvasRect.left === canvasRect.left &&
|
|
1588
|
+
previous.canvasRect.top === canvasRect.top &&
|
|
1589
|
+
previous.canvasRect.width === canvasRect.width &&
|
|
1590
|
+
previous.canvasRect.height === canvasRect.height &&
|
|
1591
|
+
sameComputerTarget(previous.frame, start.frame) &&
|
|
1592
|
+
previous.frame.width === start.frame.width &&
|
|
1593
|
+
previous.frame.height === start.frame.height
|
|
1594
|
+
: Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
|
|
1595
|
+
sameFrameFence(previous.frame, start.frame))
|
|
1519
1596
|
) {
|
|
1520
1597
|
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
1521
1598
|
clickTimerRef.current = null;
|
|
@@ -1524,15 +1601,47 @@ function ComputerViewport(props: {
|
|
|
1524
1601
|
{
|
|
1525
1602
|
type: "pointer",
|
|
1526
1603
|
frameId: start.frame.frameId,
|
|
1527
|
-
action: "double_click",
|
|
1604
|
+
action: props.pointerClickContinuation ? "click" : "double_click",
|
|
1605
|
+
...(props.pointerClickContinuation ? { clickCount: 2 as const } : {}),
|
|
1606
|
+
...(previous.delivery
|
|
1607
|
+
? { continuationOfOperationId: previous.delivery.operationId }
|
|
1608
|
+
: {}),
|
|
1609
|
+
x: to.x,
|
|
1610
|
+
y: to.y,
|
|
1611
|
+
},
|
|
1612
|
+
start.frame,
|
|
1613
|
+
undefined,
|
|
1614
|
+
previous.delivery ?? undefined,
|
|
1615
|
+
);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
flushPendingClick();
|
|
1619
|
+
lastClickRef.current = {
|
|
1620
|
+
at: now,
|
|
1621
|
+
x: to.x,
|
|
1622
|
+
y: to.y,
|
|
1623
|
+
frame: start.frame,
|
|
1624
|
+
clientX: event.clientX,
|
|
1625
|
+
clientY: event.clientY,
|
|
1626
|
+
canvasRect,
|
|
1627
|
+
pending: !props.pointerClickContinuation,
|
|
1628
|
+
delivery: null,
|
|
1629
|
+
};
|
|
1630
|
+
if (props.pointerClickContinuation) {
|
|
1631
|
+
const delivery = enqueue(
|
|
1632
|
+
{
|
|
1633
|
+
type: "pointer",
|
|
1634
|
+
frameId: start.frame.frameId,
|
|
1635
|
+
action: "click",
|
|
1636
|
+
clickCount: 1,
|
|
1528
1637
|
x: to.x,
|
|
1529
1638
|
y: to.y,
|
|
1530
1639
|
},
|
|
1531
1640
|
start.frame,
|
|
1532
1641
|
);
|
|
1642
|
+
if (lastClickRef.current) lastClickRef.current.delivery = delivery;
|
|
1533
1643
|
return;
|
|
1534
1644
|
}
|
|
1535
|
-
lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
|
|
1536
1645
|
clickTimerRef.current = setTimeout(() => {
|
|
1537
1646
|
clickTimerRef.current = null;
|
|
1538
1647
|
lastClickRef.current = null;
|
|
@@ -230,7 +230,7 @@ const baseComponents: Components = {
|
|
|
230
230
|
),
|
|
231
231
|
td: ({ children, ...props }) => (
|
|
232
232
|
<td
|
|
233
|
-
className="border-b border-og-border/70 px-0 py-1.5 pr-4 align-top text-og-fg-muted
|
|
233
|
+
className="border-b border-og-border/70 px-0 py-1.5 pr-4 align-top text-og-fg-muted"
|
|
234
234
|
{...props}
|
|
235
235
|
>
|
|
236
236
|
{children}
|
|
@@ -615,7 +615,12 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
|
|
|
615
615
|
<div className="overflow-x-auto" tabIndex={0}>
|
|
616
616
|
<table
|
|
617
617
|
ref={tableRef}
|
|
618
|
-
className={cn(
|
|
618
|
+
className={cn(
|
|
619
|
+
// Leading `&` only: a trailing one (`tr:last-child>&`) under the scoped
|
|
620
|
+
// selector list needs `:is()`, which bundlers warn about for older targets.
|
|
621
|
+
"w-full min-w-0 border-collapse text-og-base [&_tr:last-child>td]:border-b-0",
|
|
622
|
+
className,
|
|
623
|
+
)}
|
|
619
624
|
{...props}
|
|
620
625
|
>
|
|
621
626
|
{children}
|
|
@@ -2496,6 +2496,16 @@ export function MessageTimeline({
|
|
|
2496
2496
|
}
|
|
2497
2497
|
releasePinAfterScrollSettled(node);
|
|
2498
2498
|
};
|
|
2499
|
+
// React 18 has no `onScrollEnd` prop: it warns and never calls it, while
|
|
2500
|
+
// `supportsScrollEndEvent()` still makes the camera wait for scrollend. A
|
|
2501
|
+
// native listener (re-bound each commit, so it always sees this render's
|
|
2502
|
+
// handler and scroller) works on every supported React version.
|
|
2503
|
+
useEffect(() => {
|
|
2504
|
+
const node = scrollRef.current;
|
|
2505
|
+
if (!node) return;
|
|
2506
|
+
node.addEventListener("scrollend", onScrollEnd);
|
|
2507
|
+
return () => node.removeEventListener("scrollend", onScrollEnd);
|
|
2508
|
+
});
|
|
2499
2509
|
|
|
2500
2510
|
const timeline = (
|
|
2501
2511
|
<AllowancePresentationProvider
|
|
@@ -2538,7 +2548,6 @@ export function MessageTimeline({
|
|
|
2538
2548
|
}
|
|
2539
2549
|
tabIndex={-1}
|
|
2540
2550
|
onScroll={onScroll}
|
|
2541
|
-
onScrollEnd={onScrollEnd}
|
|
2542
2551
|
onWheel={onWheel}
|
|
2543
2552
|
onTouchStart={(event) => {
|
|
2544
2553
|
stopSettlement();
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
createWorkspaceRetainedArtifactLoader,
|
|
35
35
|
createWorkspaceRetainedVideoLoader,
|
|
36
36
|
} from "../timeline/retained-loaders";
|
|
37
|
+
import { useRealtimeVoiceModels } from "../hooks/use-realtime-voice-models";
|
|
37
38
|
import { projectPendingApprovals } from "../approvals";
|
|
38
39
|
import { ApprovalSurface } from "./approval-surface";
|
|
39
40
|
import { ChatComposer, type ChatComposerProps } from "./chat-composer";
|
|
@@ -126,6 +127,12 @@ export type SessionConversationProps = ClientOverride &
|
|
|
126
127
|
* (`createSessionProxyHandler({ voiceInput: false })` turns it off).
|
|
127
128
|
*/
|
|
128
129
|
voiceInput?: boolean | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Live speech-to-speech voice in the composer. Defaults to true; the voice
|
|
132
|
+
* button appears only when the workspace offers an available voice model
|
|
133
|
+
* and the proxy has not turned it off (`realtimeVoice: false`).
|
|
134
|
+
*/
|
|
135
|
+
realtimeVoice?: boolean | undefined;
|
|
129
136
|
/**
|
|
130
137
|
* Show the model/reasoning picker. End users of an embedded product rarely
|
|
131
138
|
* choose models, so it is hidden unless this is `true` or the client config
|
|
@@ -210,6 +217,7 @@ function Conversation({
|
|
|
210
217
|
allowanceExhaustedLabels,
|
|
211
218
|
attachments: attachmentsRequested = true,
|
|
212
219
|
voiceInput: voiceInputRequested = true,
|
|
220
|
+
realtimeVoice: realtimeVoiceRequested = true,
|
|
213
221
|
modelPicker,
|
|
214
222
|
modelPickerProps,
|
|
215
223
|
userMessageDisclosureLabels,
|
|
@@ -319,6 +327,30 @@ function Conversation({
|
|
|
319
327
|
if (feed.error) void feed.jumpToLatest();
|
|
320
328
|
};
|
|
321
329
|
const running = status === "running" || status === "recovering" || status === "waiting_capacity";
|
|
330
|
+
const voiceModels = useRealtimeVoiceModels(
|
|
331
|
+
context.client,
|
|
332
|
+
context.workspaceId,
|
|
333
|
+
realtimeVoiceRequested && config.realtimeVoice && !importedArchive,
|
|
334
|
+
);
|
|
335
|
+
const [voiceActive, setVoiceActive] = useState(false);
|
|
336
|
+
const voiceControl =
|
|
337
|
+
composer.effectiveControl ?? queue.effectiveControl ?? detail.session?.effectiveControl;
|
|
338
|
+
const voice =
|
|
339
|
+
voiceModels.length > 0 && status && !terminal && voiceControl ? (
|
|
340
|
+
<Suspense fallback={null}>
|
|
341
|
+
<LazyEmbeddedRealtimeVoice
|
|
342
|
+
client={context.client}
|
|
343
|
+
workspaceId={context.workspaceId}
|
|
344
|
+
models={voiceModels}
|
|
345
|
+
sessionId={sessionId}
|
|
346
|
+
sessionStatus={status}
|
|
347
|
+
effectiveControl={voiceControl}
|
|
348
|
+
events={feed.events}
|
|
349
|
+
eventsReady={!feed.initialLoading}
|
|
350
|
+
onVoiceActiveChange={setVoiceActive}
|
|
351
|
+
/>
|
|
352
|
+
</Suspense>
|
|
353
|
+
) : null;
|
|
322
354
|
return (
|
|
323
355
|
<div
|
|
324
356
|
className={cn(
|
|
@@ -467,6 +499,17 @@ function Conversation({
|
|
|
467
499
|
{...(transcription && composerProps?.transcription === undefined
|
|
468
500
|
? { transcription }
|
|
469
501
|
: {})}
|
|
502
|
+
actionsStart={
|
|
503
|
+
voice ? (
|
|
504
|
+
<>
|
|
505
|
+
{composerProps?.actionsStart}
|
|
506
|
+
{voice}
|
|
507
|
+
</>
|
|
508
|
+
) : (
|
|
509
|
+
composerProps?.actionsStart
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
transcriptionSuppressed={composerProps?.transcriptionSuppressed ?? voiceActive}
|
|
470
513
|
composer={composer}
|
|
471
514
|
attachments={uploadsEnabled ? files : undefined}
|
|
472
515
|
disabled={terminal || composerProps?.disabled}
|
|
@@ -534,6 +577,7 @@ function GoalChrome({ sessionId, client, workspaceId, events, chrome }: Conversa
|
|
|
534
577
|
});
|
|
535
578
|
return <SessionChrome {...chrome} goal={goal} />;
|
|
536
579
|
}
|
|
580
|
+
const LazyEmbeddedRealtimeVoice = lazy(() => import("../realtime/embedded-voice"));
|
|
537
581
|
|
|
538
582
|
const LazyChatInteractiveBlock = lazy(() =>
|
|
539
583
|
import("./artifacts/chat-interactive-block").then((module) => ({
|