@opengeni/react 1.3.0 → 1.4.2
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 +29 -1
- 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-WGJ43YGI.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-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-TMYGL6X5.js → chunk-55DXD22O.js} +73 -7
- package/dist/chunk-55DXD22O.js.map +1 -0
- 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-PGSX7UCP.js → chunk-CREMJMVN.js} +1502 -833
- package/dist/chunk-CREMJMVN.js.map +1 -0
- 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-Y2PRMYLC.js → chunk-G3U2A6GS.js} +3 -2
- package/dist/{chunk-Y2PRMYLC.js.map → chunk-G3U2A6GS.js.map} +1 -1
- package/dist/{chunk-WWEUO2PL.js → chunk-G6YRCIYJ.js} +202 -14
- package/dist/chunk-G6YRCIYJ.js.map +1 -0
- 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-FZCP527Y.js → chunk-HXYOSSQO.js} +3 -2
- package/dist/{chunk-FZCP527Y.js.map → chunk-HXYOSSQO.js.map} +1 -1
- 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-CY7WK5FD.js → chunk-IUHHPMAX.js} +203 -46
- 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-GYVRFZDY.js → chunk-MDIXFKA3.js} +9 -8
- package/dist/{chunk-GYVRFZDY.js.map → chunk-MDIXFKA3.js.map} +1 -1
- 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-RC6BZ44Q.js → chunk-QMMCJGFI.js} +4 -3
- package/dist/{chunk-RC6BZ44Q.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-ERGYJIBB.js → chunk-T46QZH3E.js} +4 -2
- package/dist/{chunk-ERGYJIBB.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-742CZ3L3.js → chunk-UDPH5SLV.js} +19 -202
- package/dist/chunk-UDPH5SLV.js.map +1 -0
- package/dist/{chunk-HDJWDK7W.js → chunk-VCSERYXH.js} +10 -9
- package/dist/{chunk-HDJWDK7W.js.map → chunk-VCSERYXH.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 +7 -0
- package/dist/components/open-geni-chat.d.ts +6 -2
- package/dist/components/session-conversation.d.ts +31 -19
- package/dist/components/session-list.d.ts +4 -1
- package/dist/components/tool-action-review.d.ts +29 -2
- package/dist/components/tool-review-history.d.ts +8 -4
- package/dist/composer-voice-events.d.ts +8 -0
- package/dist/composer.js +15 -13
- package/dist/computer-viewer-46UIN7F3.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 +43 -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.d.ts +3 -3
- package/dist/index.js +60 -48
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +14 -13
- package/dist/interaction.js.map +1 -1
- package/dist/{latest-question-YTOBHCWR.js → latest-question-2POFPYNB.js} +5 -4
- package/dist/{latest-question-YTOBHCWR.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/realtime-control.d.ts +2 -0
- package/dist/realtime.js +22 -1332
- package/dist/realtime.js.map +1 -1
- package/dist/sandbox-files-34CJJNOY.js +21 -0
- package/dist/session-capability-policy.d.ts +20 -1
- package/dist/session-ui.js +32 -30
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +12 -11
- package/dist/sites.js +2 -1
- package/dist/terminal.js +6 -5
- package/dist/terminal.js.map +1 -1
- package/dist/timeline/index.d.ts +2 -0
- package/dist/timeline/retained-loaders.d.ts +27 -0
- 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/approval-surface.tsx +190 -79
- package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
- package/src/components/composer-transcription-control.tsx +121 -25
- package/src/components/composer.tsx +1 -0
- package/src/components/markdown.tsx +7 -2
- package/src/components/message-timeline.tsx +10 -1
- package/src/components/open-geni-chat.tsx +53 -13
- package/src/components/session-conversation.tsx +273 -63
- package/src/components/session-list.tsx +9 -2
- package/src/components/session-proxy-scope.tsx +26 -5
- package/src/components/tool-action-review.tsx +502 -239
- package/src/components/tool-review-history.tsx +18 -43
- package/src/composer-voice-events.ts +12 -0
- package/src/connection-discovery.tsx +4 -1
- package/src/hooks/use-client-config-flags.ts +110 -0
- package/src/hooks/use-goal.ts +12 -1
- package/src/hooks/use-realtime-voice-models.ts +67 -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 +115 -18
- package/src/index.ts +9 -0
- package/src/realtime/embedded-voice.tsx +81 -0
- package/src/realtime/realtime-control.tsx +109 -17
- package/src/session-capability-policy.ts +119 -6
- 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 +143 -10
- package/src/voice-recording-owner.ts +22 -0
- package/styles/compiled.css +135 -37
- package/styles/index.css +49 -0
- package/styles/responsive.css +61 -4
- package/styles/session-capability.css +1 -1
- package/styles/tokens.css +1 -1
- package/dist/browser-viewer-SHYKQRNX.js +0 -16
- package/dist/chat-interactive-block-UGBTNB3L.js +0 -11
- package/dist/chunk-742CZ3L3.js.map +0 -1
- package/dist/chunk-CY7WK5FD.js.map +0 -1
- package/dist/chunk-I7IWRQLN.js.map +0 -1
- package/dist/chunk-IWIDV7SB.js +0 -1
- package/dist/chunk-PGSX7UCP.js.map +0 -1
- package/dist/chunk-TMYGL6X5.js.map +0 -1
- package/dist/chunk-WWEUO2PL.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-WGJ43YGI.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-46UIN7F3.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,4 +1,6 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { dequal } from "dequal";
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon, ShieldCheckIcon } from "lucide-react";
|
|
2
4
|
import {
|
|
3
5
|
OpenGeniApiError,
|
|
4
6
|
toolReviewAction,
|
|
@@ -13,6 +15,9 @@ import { toolDisplayName } from "../timeline/tool-display-name";
|
|
|
13
15
|
import {
|
|
14
16
|
ToolActionReviewCard,
|
|
15
17
|
ToolActionReviewDetails,
|
|
18
|
+
ToolReviewFrame,
|
|
19
|
+
DeclineButton,
|
|
20
|
+
UNRECOVERABLE_TEXT,
|
|
16
21
|
type ToolReviewDetailsLoader,
|
|
17
22
|
} from "./tool-action-review";
|
|
18
23
|
|
|
@@ -37,10 +42,15 @@ export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
|
|
|
37
42
|
formatToolName: (name) =>
|
|
38
43
|
name.includes("__") || /^[a-f0-9]{64}$/.test(name)
|
|
39
44
|
? toolDisplayName(name)
|
|
40
|
-
: name.replaceAll("_", " ").replaceAll(".", " › "),
|
|
45
|
+
: humanToolName(name.replaceAll("_", " ").replaceAll(".", " › ")),
|
|
41
46
|
showDetails: "View full details",
|
|
42
47
|
hideDetails: "Back to review",
|
|
43
48
|
};
|
|
49
|
+
/** "batch modify messages" → "Modify messages": how a call is grouped is not what it does. */
|
|
50
|
+
function humanToolName(name: string): string {
|
|
51
|
+
const phrase = name.replace(/^batch\s+/i, "").trim();
|
|
52
|
+
return phrase ? phrase.charAt(0).toUpperCase() + phrase.slice(1) : name;
|
|
53
|
+
}
|
|
44
54
|
export type ApprovalSurfaceProps = {
|
|
45
55
|
approvals: PendingApproval[];
|
|
46
56
|
onApprove: (approval: PendingApproval) => void | Promise<void>;
|
|
@@ -88,9 +98,10 @@ export function ApprovalSurface({
|
|
|
88
98
|
}: ApprovalSurfaceProps) {
|
|
89
99
|
const messages = { ...defaultApprovalSurfaceMessages, ...overrides };
|
|
90
100
|
const formatError = useErrorMessage();
|
|
91
|
-
const [pending, setPending] = useState<{
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
const [pending, setPending] = useState<{
|
|
102
|
+
id: string;
|
|
103
|
+
decision: "approve" | "reject";
|
|
104
|
+
} | null>(null);
|
|
94
105
|
const pendingRef = useRef<string | null>(null);
|
|
95
106
|
const [decisionError, setDecisionError] = useState<{ cause: unknown } | null>(null);
|
|
96
107
|
const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);
|
|
@@ -137,39 +148,43 @@ export function ApprovalSurface({
|
|
|
137
148
|
<section
|
|
138
149
|
ref={surface}
|
|
139
150
|
aria-label={messages.title}
|
|
140
|
-
className={cn("og-root box-border w-full min-w-0
|
|
151
|
+
className={cn("og-root box-border w-full min-w-0 space-y-2 py-2", className)}
|
|
141
152
|
data-og-approval-surface=""
|
|
142
153
|
>
|
|
143
154
|
{approvals.length > 1 && (
|
|
144
155
|
<nav
|
|
145
156
|
aria-label="Actions to review"
|
|
146
|
-
className="flex flex-wrap items-center justify-between gap-
|
|
157
|
+
className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-og-xs text-og-fg-muted"
|
|
147
158
|
>
|
|
148
|
-
<span
|
|
149
|
-
|
|
159
|
+
<span className="font-medium text-og-status-waiting">
|
|
160
|
+
{approvals.length} actions are waiting for you
|
|
161
|
+
</span>
|
|
162
|
+
<div className="flex items-center gap-1">
|
|
150
163
|
<button
|
|
151
164
|
type="button"
|
|
165
|
+
aria-label="Previous action"
|
|
152
166
|
disabled={selectedIndex === 0}
|
|
153
167
|
onClick={() => setSelectedId(approvals[selectedIndex - 1]!.id)}
|
|
154
|
-
className="min-h-
|
|
168
|
+
className="inline-flex min-h-8 min-w-8 items-center justify-center rounded-og-md hover:bg-og-hover hover:text-og-fg disabled:opacity-40 disabled:hover:bg-transparent [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
155
169
|
>
|
|
156
|
-
|
|
170
|
+
<ChevronLeftIcon className="size-4" aria-hidden="true" />
|
|
157
171
|
</button>
|
|
158
|
-
<span>
|
|
172
|
+
<span className="tabular-nums" aria-live="polite">
|
|
159
173
|
{selectedIndex + 1} of {approvals.length}
|
|
160
174
|
</span>
|
|
161
175
|
<button
|
|
162
176
|
type="button"
|
|
177
|
+
aria-label="Next action"
|
|
163
178
|
disabled={selectedIndex === approvals.length - 1}
|
|
164
179
|
onClick={() => setSelectedId(approvals[selectedIndex + 1]!.id)}
|
|
165
|
-
className="min-h-
|
|
180
|
+
className="inline-flex min-h-8 min-w-8 items-center justify-center rounded-og-md hover:bg-og-hover hover:text-og-fg disabled:opacity-40 disabled:hover:bg-transparent [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
166
181
|
>
|
|
167
|
-
|
|
182
|
+
<ChevronRightIcon className="size-4" aria-hidden="true" />
|
|
168
183
|
</button>
|
|
169
184
|
</div>
|
|
170
185
|
</nav>
|
|
171
186
|
)}
|
|
172
|
-
<div
|
|
187
|
+
<div>
|
|
173
188
|
{[selected].map((approval) => (
|
|
174
189
|
<ApprovalRequest
|
|
175
190
|
key={approval.id}
|
|
@@ -187,7 +202,7 @@ export function ApprovalSurface({
|
|
|
187
202
|
))}
|
|
188
203
|
</div>
|
|
189
204
|
{errorText && (
|
|
190
|
-
<p role="alert" className="m-0
|
|
205
|
+
<p role="alert" className="m-0 px-1 text-og-sm text-og-status-failed">
|
|
191
206
|
{errorText}
|
|
192
207
|
</p>
|
|
193
208
|
)}
|
|
@@ -215,96 +230,197 @@ function ApprovalRequest({
|
|
|
215
230
|
onApprove: () => void;
|
|
216
231
|
onReject: () => void;
|
|
217
232
|
}) {
|
|
218
|
-
const [loaded, setLoaded] = useState<ToolActionReview | null>(null);
|
|
219
|
-
const [failed, setFailed] = useState(false);
|
|
220
|
-
const [legacy, setLegacy] = useState(false);
|
|
221
233
|
const [retry, setRetry] = useState(0);
|
|
234
|
+
// Event projection recreates the same approval on unrelated timeline updates.
|
|
235
|
+
// Keep one request for equal facts, but never reuse it across changed facts,
|
|
236
|
+
// an explicit retry, or a loader change (the host's client/access boundary).
|
|
237
|
+
const requestRef = useRef({ approval, loadReview, retry });
|
|
238
|
+
if (
|
|
239
|
+
requestRef.current.loadReview !== loadReview ||
|
|
240
|
+
requestRef.current.retry !== retry ||
|
|
241
|
+
!dequal(requestRef.current.approval, approval)
|
|
242
|
+
) {
|
|
243
|
+
requestRef.current = { approval, loadReview, retry };
|
|
244
|
+
}
|
|
245
|
+
const request = requestRef.current;
|
|
246
|
+
const [result, setResult] = useState<{
|
|
247
|
+
request: typeof request;
|
|
248
|
+
review?: ToolActionReview;
|
|
249
|
+
failed?: boolean;
|
|
250
|
+
legacy?: boolean;
|
|
251
|
+
denied?: boolean;
|
|
252
|
+
} | null>(null);
|
|
253
|
+
// Hide stale facts during render, before passive effect cleanup can run.
|
|
254
|
+
const current = result?.request === request ? result : null;
|
|
255
|
+
const loaded = current?.review;
|
|
256
|
+
const failed = current?.failed;
|
|
257
|
+
const legacy = current?.legacy;
|
|
258
|
+
const denied = current?.denied;
|
|
222
259
|
const [detailPath, setDetailPath] = useState<string | null>(null);
|
|
223
260
|
useEffect(() => {
|
|
224
261
|
let active = true;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
setLegacy(false);
|
|
228
|
-
if (loadReview)
|
|
229
|
-
void loadReview(approval).then(
|
|
262
|
+
if (request.loadReview)
|
|
263
|
+
void request.loadReview(request.approval).then(
|
|
230
264
|
(review) => {
|
|
231
265
|
if (active) {
|
|
232
|
-
|
|
233
|
-
|
|
266
|
+
setResult(
|
|
267
|
+
review.id !== request.approval.id ? { request, failed: true } : { request, review },
|
|
268
|
+
);
|
|
234
269
|
}
|
|
235
270
|
},
|
|
236
271
|
(error) => {
|
|
237
272
|
if (active) {
|
|
238
|
-
|
|
273
|
+
// 404: no saved review (older or non-connector actions); use the
|
|
274
|
+
// approval's own arguments. 401/403: this person cannot review it.
|
|
275
|
+
if (error instanceof OpenGeniApiError && error.status === 404)
|
|
276
|
+
setResult({ request, legacy: true });
|
|
277
|
+
else if (
|
|
239
278
|
error instanceof OpenGeniApiError &&
|
|
240
|
-
error.status ===
|
|
241
|
-
approval.arguments !== undefined
|
|
279
|
+
(error.status === 401 || error.status === 403)
|
|
242
280
|
)
|
|
243
|
-
|
|
244
|
-
else
|
|
281
|
+
setResult({ request, failed: true, denied: true });
|
|
282
|
+
else setResult({ request, failed: true });
|
|
245
283
|
}
|
|
246
284
|
},
|
|
247
285
|
);
|
|
248
286
|
return () => {
|
|
249
287
|
active = false;
|
|
250
288
|
};
|
|
251
|
-
}, [
|
|
289
|
+
}, [request]);
|
|
290
|
+
const stableApproval = request.approval;
|
|
291
|
+
const { formatToolName, approve: approveLabel } = messages;
|
|
292
|
+
const fallback = useMemo<ToolActionReview>(
|
|
293
|
+
() => ({
|
|
294
|
+
version: 1,
|
|
295
|
+
id: stableApproval.id,
|
|
296
|
+
actionDigest: "",
|
|
297
|
+
revision: "legacy",
|
|
298
|
+
status: "pending",
|
|
299
|
+
...toolReviewAction(stableApproval.name, stableApproval.arguments, {
|
|
300
|
+
kind: "generic",
|
|
301
|
+
title:
|
|
302
|
+
stableApproval.display?.title ??
|
|
303
|
+
formatToolName(stableApproval.display?.toolName ?? stableApproval.name),
|
|
304
|
+
}),
|
|
305
|
+
...(stableApproval.display?.accountLabel
|
|
306
|
+
? { accountLabel: stableApproval.display.accountLabel }
|
|
307
|
+
: {}),
|
|
308
|
+
...toolReviewFields(stableApproval.arguments),
|
|
309
|
+
reason: "",
|
|
310
|
+
createdAt: "",
|
|
311
|
+
updatedAt: "",
|
|
312
|
+
approveLabel,
|
|
313
|
+
availableActions: stableApproval.arguments === undefined ? [] : ["approve", "reject"],
|
|
314
|
+
detailsAvailable: stableApproval.arguments !== undefined,
|
|
315
|
+
}),
|
|
316
|
+
[stableApproval, formatToolName, approveLabel],
|
|
317
|
+
);
|
|
318
|
+
const loadLegacyDetails = useCallback<ToolReviewDetailsLoader>(
|
|
319
|
+
async (value, path, offset) => ({
|
|
320
|
+
version: 1,
|
|
321
|
+
id: value.id,
|
|
322
|
+
actionDigest: value.actionDigest,
|
|
323
|
+
...toolReviewDetails(stableApproval.arguments, undefined, path, offset),
|
|
324
|
+
}),
|
|
325
|
+
[stableApproval],
|
|
326
|
+
);
|
|
252
327
|
if (loadReview && !loaded && !failed && !legacy)
|
|
253
328
|
return (
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
329
|
+
<ToolReviewFrame
|
|
330
|
+
tone="waiting"
|
|
331
|
+
icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
|
|
332
|
+
eyebrow={messages.title}
|
|
333
|
+
title={fallback.title}
|
|
334
|
+
subtitle={fallback.accountLabel}
|
|
335
|
+
aria-busy
|
|
336
|
+
data-og-tool-review-loading=""
|
|
337
|
+
>
|
|
338
|
+
<div role="status" className="space-y-2">
|
|
339
|
+
<span className="sr-only">Loading action details…</span>
|
|
340
|
+
<div className="h-3 w-3/4 animate-pulse rounded-og-sm bg-og-fg/8" aria-hidden="true" />
|
|
341
|
+
<div className="h-3 w-1/2 animate-pulse rounded-og-sm bg-og-fg/8" aria-hidden="true" />
|
|
342
|
+
</div>
|
|
343
|
+
</ToolReviewFrame>
|
|
344
|
+
);
|
|
345
|
+
if (denied)
|
|
346
|
+
return (
|
|
347
|
+
<ToolReviewFrame
|
|
348
|
+
tone="waiting"
|
|
349
|
+
icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
|
|
350
|
+
eyebrow={messages.title}
|
|
351
|
+
title={fallback.title}
|
|
352
|
+
subtitle={fallback.accountLabel}
|
|
353
|
+
>
|
|
354
|
+
<p role="alert" className="m-0 text-og-sm text-og-fg">
|
|
355
|
+
You don't have access to review this action. Ask someone with access to this chat to
|
|
356
|
+
decide.
|
|
357
|
+
</p>
|
|
358
|
+
</ToolReviewFrame>
|
|
257
359
|
);
|
|
258
360
|
if (failed)
|
|
259
361
|
return (
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
362
|
+
<ToolReviewFrame
|
|
363
|
+
tone="waiting"
|
|
364
|
+
icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
|
|
365
|
+
eyebrow={messages.title}
|
|
366
|
+
title={fallback.title}
|
|
367
|
+
subtitle={fallback.accountLabel}
|
|
368
|
+
footer={
|
|
369
|
+
<div className="ml-auto">
|
|
370
|
+
<DeclineButton
|
|
371
|
+
busy={Boolean(actions.disabled || actions.submitting)}
|
|
372
|
+
submitting={actions.submitting === "reject"}
|
|
373
|
+
label={messages.reject}
|
|
374
|
+
onClick={actions.onReject}
|
|
375
|
+
/>
|
|
376
|
+
</div>
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
<p role="alert" className="m-0 text-og-sm text-og-fg">
|
|
380
|
+
Couldn't load this action, so it can't be approved yet. You can still decline it.{" "}
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
className="min-h-8 font-medium underline underline-offset-4 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
384
|
+
onClick={() => setRetry((value) => value + 1)}
|
|
385
|
+
>
|
|
386
|
+
Try again
|
|
387
|
+
</button>
|
|
388
|
+
</p>
|
|
389
|
+
</ToolReviewFrame>
|
|
390
|
+
);
|
|
391
|
+
if (!loaded && stableApproval.arguments === undefined)
|
|
392
|
+
return (
|
|
393
|
+
<div data-approval-id={approval.id} data-review-origin="pending">
|
|
394
|
+
{custom}
|
|
395
|
+
<ToolReviewFrame
|
|
396
|
+
tone="waiting"
|
|
397
|
+
icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
|
|
398
|
+
eyebrow={messages.title}
|
|
399
|
+
title={fallback.title}
|
|
400
|
+
subtitle={fallback.accountLabel}
|
|
401
|
+
data-og-tool-review=""
|
|
402
|
+
footer={
|
|
403
|
+
<div className="ml-auto">
|
|
404
|
+
<DeclineButton
|
|
405
|
+
busy={Boolean(actions.disabled || actions.submitting)}
|
|
406
|
+
submitting={actions.submitting === "reject"}
|
|
407
|
+
label={messages.reject}
|
|
408
|
+
onClick={actions.onReject}
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
}
|
|
266
412
|
>
|
|
267
|
-
|
|
268
|
-
</
|
|
413
|
+
<p className="m-0 text-og-sm text-og-fg">{UNRECOVERABLE_TEXT}</p>
|
|
414
|
+
</ToolReviewFrame>
|
|
269
415
|
</div>
|
|
270
416
|
);
|
|
271
|
-
const fallback: ToolActionReview = {
|
|
272
|
-
version: 1,
|
|
273
|
-
id: approval.id,
|
|
274
|
-
actionDigest: "",
|
|
275
|
-
revision: "legacy",
|
|
276
|
-
status: "pending",
|
|
277
|
-
...toolReviewAction(approval.name, approval.arguments, {
|
|
278
|
-
kind: "generic",
|
|
279
|
-
title:
|
|
280
|
-
approval.display?.title ??
|
|
281
|
-
messages.formatToolName(approval.display?.toolName ?? approval.name),
|
|
282
|
-
}),
|
|
283
|
-
...(approval.display?.accountLabel ? { accountLabel: approval.display.accountLabel } : {}),
|
|
284
|
-
...toolReviewFields(approval.arguments),
|
|
285
|
-
reason: "This action requires your approval.",
|
|
286
|
-
createdAt: "",
|
|
287
|
-
updatedAt: "",
|
|
288
|
-
approveLabel: messages.approve,
|
|
289
|
-
availableActions: approval.arguments === undefined ? [] : ["approve", "reject"],
|
|
290
|
-
detailsAvailable: approval.arguments !== undefined,
|
|
291
|
-
};
|
|
292
417
|
const review = loaded ?? fallback;
|
|
293
418
|
if (detailPath !== null)
|
|
294
419
|
return (
|
|
295
420
|
<ToolActionReviewDetails
|
|
296
421
|
review={review}
|
|
297
422
|
path={detailPath}
|
|
298
|
-
load={
|
|
299
|
-
loaded && loadDetails
|
|
300
|
-
? loadDetails
|
|
301
|
-
: async (value, path, offset) => ({
|
|
302
|
-
version: 1,
|
|
303
|
-
id: value.id,
|
|
304
|
-
actionDigest: value.actionDigest,
|
|
305
|
-
...toolReviewDetails(approval.arguments, undefined, path, offset),
|
|
306
|
-
})
|
|
307
|
-
}
|
|
423
|
+
load={loaded && loadDetails ? loadDetails : loadLegacyDetails}
|
|
308
424
|
onBack={() => {
|
|
309
425
|
const path = detailPath;
|
|
310
426
|
setDetailPath(null);
|
|
@@ -340,11 +456,6 @@ function ApprovalRequest({
|
|
|
340
456
|
: undefined
|
|
341
457
|
}
|
|
342
458
|
/>
|
|
343
|
-
{!loaded && approval.arguments === undefined && (
|
|
344
|
-
<p className="pb-3 text-og-sm text-og-fg-muted">
|
|
345
|
-
Open the task with a compatible client to review this action.
|
|
346
|
-
</p>
|
|
347
|
-
)}
|
|
348
459
|
</div>
|
|
349
460
|
);
|
|
350
461
|
}
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -50,6 +58,9 @@ export type ComposerTranscriptionMessages = {
|
|
|
50
58
|
errorPermissionDenied: string;
|
|
51
59
|
errorNotSupported: string;
|
|
52
60
|
errorUnavailable: string;
|
|
61
|
+
errorInsufficientCredits: string;
|
|
62
|
+
errorAllowanceExhausted: string;
|
|
63
|
+
errorPolicyBlocked: string;
|
|
53
64
|
errorTooLarge: string;
|
|
54
65
|
errorInvalidAudio: string;
|
|
55
66
|
errorStorageUnavailable: string;
|
|
@@ -61,28 +72,34 @@ export type ComposerTranscriptionMessages = {
|
|
|
61
72
|
const defaultMessages: ComposerTranscriptionMessages = {
|
|
62
73
|
start: "Start voice input",
|
|
63
74
|
stop: "Stop and transcribe",
|
|
64
|
-
cancel: "
|
|
75
|
+
cancel: "Stop without transcribing",
|
|
65
76
|
retry: "Retry voice input",
|
|
77
|
+
transcribeSaved: "Transcribe saved recording",
|
|
78
|
+
timeRemaining: "Stops and transcribes in {seconds}s",
|
|
66
79
|
pauseRetrying: "Pause automatic retry",
|
|
67
80
|
requestingPermission: "Requesting microphone…",
|
|
68
|
-
recording: "Recording. Press Escape to
|
|
81
|
+
recording: "Recording. Press Escape to stop without transcribing.",
|
|
69
82
|
saving: "Saving audio locally…",
|
|
70
83
|
transcribing: "Transcribing…",
|
|
71
|
-
retrying: "
|
|
72
|
-
recovered: "Recording
|
|
73
|
-
recoveredTranscript: "Transcript saved
|
|
84
|
+
retrying: "Saved. Retrying transcription…",
|
|
85
|
+
recovered: "Recording saved on this device.",
|
|
86
|
+
recoveredTranscript: "Transcript saved. Insert it into your draft?",
|
|
74
87
|
insertRecoveredTranscript: "Insert saved transcript",
|
|
75
88
|
discardRecovered: "Discard saved recording",
|
|
76
89
|
unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
|
|
77
90
|
unavailable: "Voice input is unavailable for this workspace.",
|
|
78
|
-
errorPermissionDenied: "Microphone
|
|
91
|
+
errorPermissionDenied: "Microphone access is blocked. Allow it in site settings, then try again.",
|
|
79
92
|
errorNotSupported: "Voice input is not supported on this device.",
|
|
80
93
|
errorUnavailable: "Voice input is not configured.",
|
|
94
|
+
errorInsufficientCredits:
|
|
95
|
+
"Voice input needs credits. Recording saved; retry after adding credits.",
|
|
96
|
+
errorAllowanceExhausted: "Usage limit reached. Recording saved for later.",
|
|
97
|
+
errorPolicyBlocked: "Voice input isn't allowed for this account or workspace.",
|
|
81
98
|
errorTooLarge: "Recording is too large. Try a shorter message.",
|
|
82
99
|
errorInvalidAudio: "The recording could not be read. Try again.",
|
|
83
100
|
errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
|
|
84
|
-
errorRetryable: "
|
|
85
|
-
errorHandoffUncertain: "Transcript
|
|
101
|
+
errorRetryable: "Saved. Transcription failed, retry when ready.",
|
|
102
|
+
errorHandoffUncertain: "Transcript saved. Check your draft before inserting again.",
|
|
86
103
|
errorUnknown: "Voice input could not start. Try again.",
|
|
87
104
|
};
|
|
88
105
|
|
|
@@ -146,12 +163,18 @@ export function ComposerTranscriptionControl({
|
|
|
146
163
|
const capturing = status === "requesting-permission" || status === "recording";
|
|
147
164
|
|
|
148
165
|
const cancelTranscription = transcription.cancel;
|
|
166
|
+
const discardTranscription = transcription.discard;
|
|
167
|
+
const hasRecoverableRecording = transcription.hasRecoverableRecording;
|
|
149
168
|
useEffect(() => {
|
|
150
169
|
if (!suppressed) return;
|
|
151
170
|
if (status === "recording" || status === "requesting-permission") {
|
|
152
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();
|
|
153
176
|
}
|
|
154
|
-
}, [cancelTranscription, status, suppressed]);
|
|
177
|
+
}, [cancelTranscription, discardTranscription, hasRecoverableRecording, status, suppressed]);
|
|
155
178
|
const active = capturing || status === "saving" || status === "transcribing";
|
|
156
179
|
const recoverable =
|
|
157
180
|
transcription.hasRecoverableRecording &&
|
|
@@ -187,11 +210,22 @@ export function ComposerTranscriptionControl({
|
|
|
187
210
|
? (errorMessage ?? messages.errorUnknown)
|
|
188
211
|
: unavailableMessage;
|
|
189
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
|
+
|
|
190
221
|
function start(event: MouseEvent<HTMLButtonElement>) {
|
|
191
222
|
if (unavailableMessage) {
|
|
192
223
|
event.preventDefault();
|
|
193
224
|
return;
|
|
194
225
|
}
|
|
226
|
+
composerVoiceEventScope(event.currentTarget)?.dispatchEvent(
|
|
227
|
+
new Event(COMPOSER_VOICE_INPUT_START_EVENT),
|
|
228
|
+
);
|
|
195
229
|
void transcription.start();
|
|
196
230
|
}
|
|
197
231
|
|
|
@@ -213,6 +247,9 @@ export function ComposerTranscriptionControl({
|
|
|
213
247
|
)}
|
|
214
248
|
data-transcription-status={status}
|
|
215
249
|
data-transcription-capturing={capturing ? "" : undefined}
|
|
250
|
+
data-transcription-attention={
|
|
251
|
+
!capturing && (recoverable || (status === "error" && errorMessage)) ? "" : undefined
|
|
252
|
+
}
|
|
216
253
|
>
|
|
217
254
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
218
255
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
@@ -228,14 +265,11 @@ export function ComposerTranscriptionControl({
|
|
|
228
265
|
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
229
266
|
)}
|
|
230
267
|
>
|
|
231
|
-
<span
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
: status === "error"
|
|
237
|
-
? (errorMessage ?? messages.errorRetryable)
|
|
238
|
-
: 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}
|
|
239
273
|
</span>
|
|
240
274
|
<Tip
|
|
241
275
|
tip={
|
|
@@ -243,7 +277,9 @@ export function ComposerTranscriptionControl({
|
|
|
243
277
|
? messages.insertRecoveredTranscript
|
|
244
278
|
: retrying
|
|
245
279
|
? messages.pauseRetrying
|
|
246
|
-
:
|
|
280
|
+
: status === "recovered"
|
|
281
|
+
? messages.transcribeSaved
|
|
282
|
+
: messages.retry
|
|
247
283
|
}
|
|
248
284
|
>
|
|
249
285
|
<button
|
|
@@ -261,7 +297,9 @@ export function ComposerTranscriptionControl({
|
|
|
261
297
|
? messages.insertRecoveredTranscript
|
|
262
298
|
: retrying
|
|
263
299
|
? messages.pauseRetrying
|
|
264
|
-
:
|
|
300
|
+
: status === "recovered"
|
|
301
|
+
? messages.transcribeSaved
|
|
302
|
+
: messages.retry
|
|
265
303
|
}
|
|
266
304
|
className={cn(
|
|
267
305
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
@@ -323,6 +361,13 @@ export function ComposerTranscriptionControl({
|
|
|
323
361
|
stream={status === "recording" ? transcription.stream : null}
|
|
324
362
|
mode={status === "recording" ? "recording" : "transcribing"}
|
|
325
363
|
/>
|
|
364
|
+
{status === "recording" && transcription.recordingStartedAt !== null ? (
|
|
365
|
+
<RecordingClock
|
|
366
|
+
startedAt={transcription.recordingStartedAt}
|
|
367
|
+
maxSeconds={transcription.maxRecordingSeconds}
|
|
368
|
+
remainingLabel={messages.timeRemaining}
|
|
369
|
+
/>
|
|
370
|
+
) : null}
|
|
326
371
|
</span>
|
|
327
372
|
)}
|
|
328
373
|
{status === "recording" ? (
|
|
@@ -394,14 +439,22 @@ export function ComposerTranscriptionControl({
|
|
|
394
439
|
)}
|
|
395
440
|
</AnimatePresence>
|
|
396
441
|
{status === "error" && errorMessage && !recoverable ? (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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">
|
|
402
454
|
{errorMessage}
|
|
403
455
|
</span>
|
|
404
|
-
|
|
456
|
+
<XIcon className="size-3 shrink-0 opacity-70" aria-hidden />
|
|
457
|
+
</button>
|
|
405
458
|
) : null}
|
|
406
459
|
<span
|
|
407
460
|
className="sr-only"
|
|
@@ -417,6 +470,42 @@ export function ComposerTranscriptionControl({
|
|
|
417
470
|
);
|
|
418
471
|
}
|
|
419
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
|
+
|
|
420
509
|
function VoiceWaveform({
|
|
421
510
|
stream,
|
|
422
511
|
mode,
|
|
@@ -518,6 +607,13 @@ function restingLevels(count: number): number[] {
|
|
|
518
607
|
|
|
519
608
|
function transcriptionErrorMessage(code: string, messages: ComposerTranscriptionMessages): string {
|
|
520
609
|
switch (code) {
|
|
610
|
+
case "insufficient_credits":
|
|
611
|
+
return messages.errorInsufficientCredits;
|
|
612
|
+
case "allowance_exhausted":
|
|
613
|
+
case "monthly_model_cost_limit":
|
|
614
|
+
return messages.errorAllowanceExhausted;
|
|
615
|
+
case "policy_blocked":
|
|
616
|
+
return messages.errorPolicyBlocked;
|
|
521
617
|
case "permission_denied":
|
|
522
618
|
return messages.errorPermissionDenied;
|
|
523
619
|
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}
|