@opengeni/react 1.4.4 → 1.4.5-canary.37458084490001
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 +17 -6
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +22 -15
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-5EEAS3IU.js → browser-viewer-CV23VIHB.js} +8 -6
- package/dist/chunk-3ZC6QPOM.js +174 -0
- package/dist/chunk-3ZC6QPOM.js.map +1 -0
- package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
- package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
- package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
- package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
- package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
- package/dist/chunk-7RRBX5EM.js.map +1 -0
- package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
- package/dist/chunk-7ZB7I3KP.js.map +1 -0
- package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
- package/dist/chunk-ATIRUHBZ.js.map +1 -0
- package/dist/chunk-BOFEQC6K.js +162 -0
- package/dist/chunk-BOFEQC6K.js.map +1 -0
- package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
- package/dist/chunk-CESBPQP5.js.map +1 -0
- package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
- package/dist/chunk-GHJU7XG7.js.map +1 -0
- package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
- package/dist/chunk-HNVWMDNN.js.map +1 -0
- package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
- package/dist/chunk-I5EGRXXG.js.map +1 -0
- package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
- package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
- package/dist/chunk-IX3K4THH.js.map +1 -0
- package/dist/chunk-IZV5FTJC.js +55 -0
- package/dist/chunk-IZV5FTJC.js.map +1 -0
- package/dist/chunk-JDJJMBB3.js +1883 -0
- package/dist/chunk-JDJJMBB3.js.map +1 -0
- package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
- package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
- package/dist/chunk-LHF6BVM4.js +83 -0
- package/dist/chunk-LHF6BVM4.js.map +1 -0
- package/dist/chunk-N7S72EXK.js +25 -0
- package/dist/chunk-N7S72EXK.js.map +1 -0
- package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
- package/dist/chunk-PEHTX3V7.js.map +1 -0
- package/dist/chunk-R5R5RQQD.js +51 -0
- package/dist/chunk-R5R5RQQD.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
- package/dist/chunk-SHLZMAJN.js.map +1 -0
- package/dist/{chunk-EAOSEIXB.js → chunk-TZQSHCZN.js} +692 -1996
- package/dist/chunk-TZQSHCZN.js.map +1 -0
- package/dist/chunk-ZIB4H5LN.js +101 -0
- package/dist/chunk-ZIB4H5LN.js.map +1 -0
- package/dist/client.d.ts +9 -3
- package/dist/clipboard.js +1 -1
- package/dist/components/browser-viewer.d.ts +6 -0
- package/dist/components/composer.d.ts +3 -69
- package/dist/components/human-input-form.d.ts +3 -32
- package/dist/components/markdown.d.ts +8 -0
- package/dist/components/model-policy-picker.d.ts +4 -37
- package/dist/components/opper-mark.d.ts +7 -0
- package/dist/components/provider-recovery-notice.d.ts +15 -0
- package/dist/components/remark-soft-line-breaks.d.ts +38 -0
- package/dist/components/sandbox-workspace.d.ts +13 -0
- package/dist/components/session-chrome.d.ts +5 -3
- package/dist/components/session-conversation.d.ts +15 -3
- package/dist/composer-messages.d.ts +74 -0
- package/dist/composer-messages.js +9 -0
- package/dist/composer.js +15 -10
- package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
- package/dist/connect-setup.d.ts +2 -0
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +21 -6
- package/dist/connect.js.map +1 -1
- package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
- package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
- package/dist/hooks/shared-machines-list.d.ts +4 -0
- package/dist/hooks/use-client-config-flags.d.ts +6 -0
- package/dist/hooks/use-goal.d.ts +2 -1
- package/dist/human-input-model.d.ts +40 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +116 -73
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +5 -5
- package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/provider-recovery.d.ts +49 -0
- package/dist/machines.js +2 -2
- package/dist/model-mark-paths.d.ts +34 -0
- package/dist/model-mark-paths.js +16 -0
- package/dist/model-picker-model.d.ts +111 -0
- package/dist/model-picker-model.js +25 -0
- package/dist/model-picker-model.js.map +1 -0
- package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
- package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
- package/dist/model-policy.d.ts +10 -0
- package/dist/model-policy.js +5 -1
- package/dist/organization-model.d.ts +32 -0
- package/dist/organization-model.js +65 -0
- package/dist/organization-model.js.map +1 -0
- package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
- package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
- package/dist/queue-presentation-model.d.ts +72 -0
- package/dist/queue-presentation-model.js +38 -0
- package/dist/queue-presentation-model.js.map +1 -0
- package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
- package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
- package/dist/realtime.js +5 -3
- package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
- package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
- package/dist/sandbox-label-model.d.ts +23 -0
- package/dist/sandbox-label-model.js +28 -0
- package/dist/sandbox-label-model.js.map +1 -0
- package/dist/session-attention-model.d.ts +8 -0
- package/dist/session-attention-model.js +46 -0
- package/dist/session-attention-model.js.map +1 -0
- package/dist/session-feedback-model.d.ts +20 -0
- package/dist/session-feedback-model.js +56 -0
- package/dist/session-feedback-model.js.map +1 -0
- package/dist/session-list-model.d.ts +96 -0
- package/dist/session-list-model.js +219 -0
- package/dist/session-list-model.js.map +1 -0
- package/dist/session-status-model.d.ts +19 -0
- package/dist/session-ui.js +26 -17
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +8 -6
- package/dist/testing/session-scenarios.d.ts +10 -0
- package/dist/testing/session-scenarios.js +303 -0
- package/dist/testing/session-scenarios.js.map +1 -0
- package/dist/timeline/genie-copy.d.ts +3 -0
- package/dist/timeline/genie-loading.d.ts +11 -0
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/notice-presentation.d.ts +7 -0
- package/dist/timeline/patch-apply-command.d.ts +18 -0
- package/dist/timeline/platform-activity-presentation.d.ts +7 -0
- package/dist/timeline/presented-image.d.ts +6 -1
- package/dist/timeline/question-nav-model.d.ts +23 -0
- package/dist/timeline/registry.d.ts +10 -1
- package/dist/timeline/retained-loaders.d.ts +1 -1
- package/dist/timeline/tool-presentation.d.ts +172 -0
- package/dist/timeline/turn-summary-model.d.ts +90 -0
- package/dist/timeline/turn-summary.d.ts +4 -74
- package/dist/timeline/work-presentation.d.ts +42 -0
- package/dist/timeline-model.d.ts +24 -0
- package/dist/timeline-model.js +152 -0
- package/dist/timeline-model.js.map +1 -0
- package/package.json +47 -3
- package/src/artifacts.ts +1 -0
- package/src/client.ts +9 -3
- package/src/components/browser-viewer.tsx +23 -2
- package/src/components/claude-mark.tsx +2 -1
- package/src/components/composer.tsx +4 -144
- package/src/components/grok-mark.tsx +2 -1
- package/src/components/human-input-form.tsx +13 -124
- package/src/components/markdown.tsx +19 -1
- package/src/components/message-timeline.tsx +45 -99
- package/src/components/model-mark.tsx +2 -1
- package/src/components/model-policy-picker-menu.tsx +30 -85
- package/src/components/model-policy-picker.tsx +15 -148
- package/src/components/opper-mark.tsx +18 -0
- package/src/components/provider-recovery-notice.tsx +41 -0
- package/src/components/queue-surface-implementation.tsx +20 -349
- package/src/components/remark-soft-line-breaks.ts +166 -0
- package/src/components/sandbox-workspace.tsx +17 -1
- package/src/components/session-chrome.tsx +114 -99
- package/src/components/session-conversation.tsx +55 -6
- package/src/components/session-status.tsx +17 -16
- package/src/composer-messages.ts +150 -0
- package/src/connect-setup.tsx +45 -11
- package/src/connect.ts +1 -1
- package/src/hooks/shared-machines-list.ts +34 -1
- package/src/hooks/use-client-config-flags.ts +8 -0
- package/src/hooks/use-goal.ts +66 -10
- package/src/human-input-model.ts +147 -0
- package/src/index.ts +16 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/format.ts +7 -0
- package/src/lib/provider-recovery.ts +202 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +43 -2
- package/src/organization-model.ts +118 -0
- package/src/queue-presentation-model.ts +464 -0
- package/src/sandbox-label-model.ts +41 -0
- package/src/session-attention-model.ts +59 -0
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +327 -0
- package/src/session-status-model.ts +62 -0
- package/src/testing/session-scenarios.ts +326 -0
- package/src/timeline/activity-rail.tsx +5 -17
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +13 -18
- package/src/timeline/index.ts +1 -0
- package/src/timeline/notice-presentation.ts +22 -0
- package/src/timeline/patch-apply-command.tsx +135 -0
- package/src/timeline/platform-activity-presentation.ts +122 -0
- package/src/timeline/platform-activity-row.tsx +6 -96
- package/src/timeline/presented-image.ts +16 -2
- package/src/timeline/question-nav-model.ts +28 -0
- package/src/timeline/registry.ts +11 -1
- package/src/timeline/retained-loaders.ts +9 -4
- package/src/timeline/rolling-activity.tsx +7 -19
- package/src/timeline/tool-presentation.ts +1316 -0
- package/src/timeline/tool-renderers.tsx +226 -1025
- package/src/timeline/turn-summary-model.ts +338 -0
- package/src/timeline/turn-summary.tsx +21 -326
- package/src/timeline/work-presentation.ts +139 -0
- package/src/timeline-model.ts +108 -0
- package/styles/compiled.css +3 -0
- package/styles/connect.css +4 -0
- package/dist/chunk-2OQA3CPB.js.map +0 -1
- package/dist/chunk-CVYCLFMH.js.map +0 -1
- package/dist/chunk-EAOSEIXB.js.map +0 -1
- package/dist/chunk-G6YRCIYJ.js.map +0 -1
- package/dist/chunk-HXYOSSQO.js +0 -117
- package/dist/chunk-HXYOSSQO.js.map +0 -1
- package/dist/chunk-IUHHPMAX.js.map +0 -1
- package/dist/chunk-IYQCWGU5.js.map +0 -1
- package/dist/chunk-JH7V627O.js.map +0 -1
- package/dist/chunk-L2J3PU7A.js.map +0 -1
- package/dist/chunk-LVS3MH5T.js.map +0 -1
- package/dist/chunk-PHBFJCEU.js.map +0 -1
- package/dist/chunk-XS67YG5R.js.map +0 -1
- package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
- package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
- /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
- /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
- /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
- /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
- /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
- /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
- /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
- /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
- /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer's user-facing copy, renderer-free so web and native composers
|
|
3
|
+
* (and hosts translating them) share one message catalog.
|
|
4
|
+
*/
|
|
5
|
+
import type { SlashCommand } from "./commands/types";
|
|
6
|
+
import { formatBytes, formatRelativeTime } from "./lib/format";
|
|
7
|
+
|
|
8
|
+
export type ChatComposerMessages = {
|
|
9
|
+
messagePlaceholder: string;
|
|
10
|
+
pausedPlaceholder: string;
|
|
11
|
+
inputLabel: string;
|
|
12
|
+
keyboardHint: string;
|
|
13
|
+
slashCommandBlocked: string;
|
|
14
|
+
controlChangedError: string;
|
|
15
|
+
sendFailedError: string;
|
|
16
|
+
dropFiles: string;
|
|
17
|
+
attachFiles: string;
|
|
18
|
+
pauseAriaLabel: string;
|
|
19
|
+
pauseTitle: string;
|
|
20
|
+
/** Stop control shown while a response runs (`runControl="stop"`). */
|
|
21
|
+
stopAriaLabel?: string | undefined;
|
|
22
|
+
stopTitle?: string | undefined;
|
|
23
|
+
sendMessageAriaLabel: string;
|
|
24
|
+
sendAndResumeAriaLabel: string;
|
|
25
|
+
sendTitle: string;
|
|
26
|
+
sendAndResumeTitle: string;
|
|
27
|
+
annotationNotesRequired: string;
|
|
28
|
+
workspacePaused: string;
|
|
29
|
+
pausedHere: string;
|
|
30
|
+
parentBlocker: string;
|
|
31
|
+
narrowerPause: string;
|
|
32
|
+
pausedBy: (displayName: string) => string;
|
|
33
|
+
queuedAhead: (count: number) => string;
|
|
34
|
+
resumingAndSending: string;
|
|
35
|
+
nextMessageResumes: string;
|
|
36
|
+
resumeThisWorkstream: string;
|
|
37
|
+
resumeShort: string;
|
|
38
|
+
hidePauseDetails: string;
|
|
39
|
+
showPauseDetails: string;
|
|
40
|
+
pauseReasonsLabel: string;
|
|
41
|
+
pausedByLabel: string;
|
|
42
|
+
alsoPausedByLabel: string;
|
|
43
|
+
resumeWorkspace: string;
|
|
44
|
+
resumeFromSession: string;
|
|
45
|
+
sessionCanRun: string;
|
|
46
|
+
stillPausedBy: (displayName: string) => string;
|
|
47
|
+
restoredResourcesLabel: string;
|
|
48
|
+
restoredFile: (fileId: string) => string;
|
|
49
|
+
removeRestoredResource: (index: number) => string;
|
|
50
|
+
uploading: string;
|
|
51
|
+
uploadFailed: string;
|
|
52
|
+
previewAttachment: (name: string) => string;
|
|
53
|
+
previewUnavailable?: string | undefined;
|
|
54
|
+
attachmentPreviewLabel: string;
|
|
55
|
+
downloadAttachment: (name: string) => string;
|
|
56
|
+
closeAttachmentPreview: string;
|
|
57
|
+
retryAttachment: (name: string) => string;
|
|
58
|
+
retryUpload: string;
|
|
59
|
+
removeAttachment: (name: string) => string;
|
|
60
|
+
confirmCommand: (name: string) => string;
|
|
61
|
+
confirmDescription: (command: SlashCommand) => string;
|
|
62
|
+
cancel: string;
|
|
63
|
+
runCommand: (name: string) => string;
|
|
64
|
+
commands: string;
|
|
65
|
+
slashCommandsLabel: string;
|
|
66
|
+
modelLabel: string;
|
|
67
|
+
close: string;
|
|
68
|
+
danger: string;
|
|
69
|
+
draftConflict: string;
|
|
70
|
+
useOtherDraft: string;
|
|
71
|
+
keepMine: string;
|
|
72
|
+
savingDraft: string;
|
|
73
|
+
formatBytes: (bytes: number) => string;
|
|
74
|
+
formatRelativeTime: (changedAt: string) => string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const defaultChatComposerMessages: ChatComposerMessages = {
|
|
78
|
+
messagePlaceholder: "Message the agent…",
|
|
79
|
+
pausedPlaceholder: "Message the agent — it will wait in the queue…",
|
|
80
|
+
inputLabel: "Message the agent",
|
|
81
|
+
// Shortcuts live in the send button's title; the footer stays quiet unless
|
|
82
|
+
// the host supplies its own hint.
|
|
83
|
+
keyboardHint: "",
|
|
84
|
+
slashCommandBlocked:
|
|
85
|
+
"That's a slash command — press Enter in the command list to run it, or edit the line to send a message.",
|
|
86
|
+
controlChangedError:
|
|
87
|
+
"This workstream was paused while you were sending. Nothing was sent, and your draft is still here.",
|
|
88
|
+
sendFailedError: "Sending failed — your draft is still here. Try again.",
|
|
89
|
+
dropFiles: "Drop files to attach",
|
|
90
|
+
attachFiles: "Attach files",
|
|
91
|
+
pauseAriaLabel: "Pause this workstream",
|
|
92
|
+
pauseTitle: "Pause this workstream; queued prompts and approvals are preserved",
|
|
93
|
+
stopAriaLabel: "Stop",
|
|
94
|
+
stopTitle: "Stop the response",
|
|
95
|
+
sendMessageAriaLabel: "Send message",
|
|
96
|
+
sendAndResumeAriaLabel: "Add message to queue",
|
|
97
|
+
sendTitle: "Queue message (Enter); steer with Cmd/Ctrl+Enter",
|
|
98
|
+
sendAndResumeTitle: "Add to queue (Enter); steer now with Cmd/Ctrl+Enter",
|
|
99
|
+
annotationNotesRequired: "Add a note to each quote before sending.",
|
|
100
|
+
workspacePaused: "Workspace paused",
|
|
101
|
+
pausedHere: "Paused here",
|
|
102
|
+
parentBlocker: "parent",
|
|
103
|
+
narrowerPause: "a narrower pause",
|
|
104
|
+
pausedBy: (displayName) => `Paused by ${displayName}`,
|
|
105
|
+
queuedAhead: (count) =>
|
|
106
|
+
`${count} waiting prompt${count === 1 ? "" : "s"}; your message joins the queue.`,
|
|
107
|
+
resumingAndSending: "Sending…",
|
|
108
|
+
nextMessageResumes: "Messages wait in the queue until you resume.",
|
|
109
|
+
resumeThisWorkstream: "Resume this workstream",
|
|
110
|
+
resumeShort: "Resume",
|
|
111
|
+
hidePauseDetails: "Hide pause details",
|
|
112
|
+
showPauseDetails: "Show pause details",
|
|
113
|
+
pauseReasonsLabel: "Reasons this workstream is paused",
|
|
114
|
+
pausedByLabel: "Paused by ",
|
|
115
|
+
alsoPausedByLabel: "Also paused by ",
|
|
116
|
+
resumeWorkspace: "Resume workspace",
|
|
117
|
+
resumeFromSession: "Resume from this session",
|
|
118
|
+
sessionCanRun: "This session will be able to run",
|
|
119
|
+
stillPausedBy: (displayName) => `Still paused by ${displayName}`,
|
|
120
|
+
restoredResourcesLabel: "Restored prompt resources",
|
|
121
|
+
restoredFile: (fileId) => `File ${fileId.slice(0, 8)}`,
|
|
122
|
+
removeRestoredResource: (index) => `Remove restored resource ${index + 1}`,
|
|
123
|
+
uploading: "Uploading",
|
|
124
|
+
uploadFailed: "Upload failed",
|
|
125
|
+
previewAttachment: (name) => `Preview ${name}`,
|
|
126
|
+
previewUnavailable: "Preview unavailable",
|
|
127
|
+
attachmentPreviewLabel: "Attachment preview",
|
|
128
|
+
downloadAttachment: (name) => `Download ${name}`,
|
|
129
|
+
closeAttachmentPreview: "Close",
|
|
130
|
+
retryAttachment: (name) => `Retry ${name}`,
|
|
131
|
+
retryUpload: "Retry upload",
|
|
132
|
+
removeAttachment: (name) => `Remove ${name}`,
|
|
133
|
+
confirmCommand: (name) => `Confirm /${name}`,
|
|
134
|
+
confirmDescription: (command) => `Run /${command.name}? ${command.description}`,
|
|
135
|
+
cancel: "Cancel",
|
|
136
|
+
runCommand: (name) => `Run /${name}`,
|
|
137
|
+
commands: "Commands",
|
|
138
|
+
slashCommandsLabel: "Slash commands",
|
|
139
|
+
modelLabel: "Model",
|
|
140
|
+
close: "Close",
|
|
141
|
+
danger: "danger",
|
|
142
|
+
draftConflict: "This draft changed in another tab. Your local draft is still here.",
|
|
143
|
+
useOtherDraft: "Use other draft",
|
|
144
|
+
keepMine: "Keep mine",
|
|
145
|
+
// Kept for embedder message overrides / back-compat. Routine autosave is
|
|
146
|
+
// silent — only draft conflicts surface under the composer.
|
|
147
|
+
savingDraft: "Saving draft…",
|
|
148
|
+
formatBytes,
|
|
149
|
+
formatRelativeTime,
|
|
150
|
+
};
|
package/src/connect-setup.tsx
CHANGED
|
@@ -19,6 +19,11 @@ const setupStatus: Record<ConnectAttempt["state"], string> = {
|
|
|
19
19
|
uncertain: "Setup outcome needs checking",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/** GitHub returned from a non-owner's install request: an organization owner must approve. */
|
|
23
|
+
export function isOwnerApprovalPending(attempt: Pick<ConnectAttempt, "error">): boolean {
|
|
24
|
+
return attempt.error?.code === "owner_approval_pending";
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function deviceVerificationUrl(action: ConnectAttempt["nextAction"]): string | null {
|
|
23
28
|
if (action.type !== "wait" || !action.verificationUrl) return null;
|
|
24
29
|
try {
|
|
@@ -91,6 +96,11 @@ function ScopedSetup({
|
|
|
91
96
|
const action = attempt.nextAction;
|
|
92
97
|
const verificationUrl = deviceVerificationUrl(action);
|
|
93
98
|
const terminal = ["complete", "cancelled", "expired"].includes(attempt.state);
|
|
99
|
+
const awaitingOwner = isOwnerApprovalPending(attempt);
|
|
100
|
+
const offersGitHubRequest =
|
|
101
|
+
action.type === "select_account" &&
|
|
102
|
+
attempt.providerId.startsWith("github") &&
|
|
103
|
+
action.accounts.some((account) => account.id === "new");
|
|
94
104
|
const submit = (event: FormEvent<HTMLFormElement>) => {
|
|
95
105
|
event.preventDefault();
|
|
96
106
|
const form = event.currentTarget;
|
|
@@ -148,7 +158,7 @@ function ScopedSetup({
|
|
|
148
158
|
{attempt.ownership === "personal" ? "Personal connection" : "Workspace connection"}
|
|
149
159
|
</p>
|
|
150
160
|
<p role="status" className="og-connect-setup-status">
|
|
151
|
-
{setupStatus[attempt.state]}
|
|
161
|
+
{awaitingOwner ? "Waiting for an organization owner" : setupStatus[attempt.state]}
|
|
152
162
|
</p>
|
|
153
163
|
<p className="og-connect-setup-description">
|
|
154
164
|
{attempt.ownership === "personal"
|
|
@@ -157,7 +167,22 @@ function ScopedSetup({
|
|
|
157
167
|
</p>
|
|
158
168
|
</div>
|
|
159
169
|
{attempt.account && <p>Account: {attempt.account.label}</p>}
|
|
160
|
-
{
|
|
170
|
+
{awaitingOwner && (
|
|
171
|
+
<div className="og-connect-setup-pending">
|
|
172
|
+
<p>
|
|
173
|
+
Your request was sent. GitHub asked the owners of your organization to approve Opengeni.
|
|
174
|
+
</p>
|
|
175
|
+
<ol>
|
|
176
|
+
<li>An organization owner approves the request on GitHub.</li>
|
|
177
|
+
<li>
|
|
178
|
+
An owner then connects the organization here. If they don't use Opengeni yet, invite
|
|
179
|
+
them to this workspace first.
|
|
180
|
+
</li>
|
|
181
|
+
</ol>
|
|
182
|
+
<p>Nothing is connected until then. You can close this and keep working.</p>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
{(localError || view.error || (attempt.error && !awaitingOwner)) && (
|
|
161
186
|
<p role="alert">
|
|
162
187
|
{attempt.error?.code === "source_changed"
|
|
163
188
|
? "The integration source changed. Review the updated tools before adding them."
|
|
@@ -172,15 +197,17 @@ function ScopedSetup({
|
|
|
172
197
|
) ? (
|
|
173
198
|
<legend>Connection details</legend>
|
|
174
199
|
) : null}
|
|
175
|
-
{attempt.state === "connected_but_incomplete" &&
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
{attempt.state === "connected_but_incomplete" &&
|
|
201
|
+
action.type === "none" &&
|
|
202
|
+
!awaitingOwner && (
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className="og-connect-setup-primary"
|
|
206
|
+
onClick={() => invoke(() => view.advance({ type: "retry" }, crypto.randomUUID()))}
|
|
207
|
+
>
|
|
208
|
+
Choose tools
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
184
211
|
{action.type === "credentials" &&
|
|
185
212
|
action.fields.map((field, i) => (
|
|
186
213
|
<label key={field.name}>
|
|
@@ -225,6 +252,13 @@ function ScopedSetup({
|
|
|
225
252
|
</select>
|
|
226
253
|
</label>
|
|
227
254
|
)}
|
|
255
|
+
{offersGitHubRequest && (
|
|
256
|
+
<p className="og-connect-setup-hint">
|
|
257
|
+
Don't see your organization? Only its owners can connect it. Choose "Install on
|
|
258
|
+
another GitHub account" and pick the organization: GitHub lets you send its owners a
|
|
259
|
+
request to approve.
|
|
260
|
+
</p>
|
|
261
|
+
)}
|
|
228
262
|
{action.type === "select_resources" &&
|
|
229
263
|
action.resources.map((resource) => (
|
|
230
264
|
<label key={resource.id}>
|
package/src/connect.ts
CHANGED
|
@@ -15,7 +15,7 @@ export {
|
|
|
15
15
|
type SkillDiscoveryItem,
|
|
16
16
|
type SkillDiscoveryPage,
|
|
17
17
|
} from "./skill-discovery";
|
|
18
|
-
export { ConnectSetup, type ConnectSetupProps } from "./connect-setup";
|
|
18
|
+
export { ConnectSetup, isOwnerApprovalPending, type ConnectSetupProps } from "./connect-setup";
|
|
19
19
|
export { ConnectChooser, type ConnectChooserProps } from "./connect-chooser";
|
|
20
20
|
export { ConnectAccounts, type ConnectAccountsProps } from "./connect-accounts";
|
|
21
21
|
export { ConnectPanel, type ConnectPanelProps } from "./connect-panel";
|
|
@@ -31,10 +31,22 @@ type Share = {
|
|
|
31
31
|
inFlight: Promise<void> | null;
|
|
32
32
|
trailing: Promise<void> | null;
|
|
33
33
|
generation: number;
|
|
34
|
+
/** The last read was refused (401/403/404). Polling stays stopped until the
|
|
35
|
+
* read is disabled (a permission/workspace change) or explicitly refreshed. */
|
|
36
|
+
halted: boolean;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
const shares = new Map<string, Share>();
|
|
37
40
|
|
|
41
|
+
/** A refusal that repeating the identical read cannot fix: the caller lacks
|
|
42
|
+
* `enrollments:read` (403), is signed out (401), or the workspace/feature is
|
|
43
|
+
* gone (404). Structural so host adapters with their own error types work. */
|
|
44
|
+
export function isMachinesAccessRefusal(cause: unknown): boolean {
|
|
45
|
+
if (!cause || typeof cause !== "object" || !("status" in cause)) return false;
|
|
46
|
+
const status = (cause as { status: unknown }).status;
|
|
47
|
+
return status === 401 || status === 403 || status === 404;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function emit(share: Share): void {
|
|
39
51
|
for (const subscriber of share.subscribers) {
|
|
40
52
|
subscriber.onChange();
|
|
@@ -81,6 +93,7 @@ function cancelDispose(share: Share): void {
|
|
|
81
93
|
|
|
82
94
|
function arm(share: Share): void {
|
|
83
95
|
stopTimer(share);
|
|
96
|
+
if (share.halted) return;
|
|
84
97
|
const { enabled, pollIntervalMs } = effective(share);
|
|
85
98
|
if (!enabled || pollIntervalMs === undefined) return;
|
|
86
99
|
share.timer = setTimeout(() => {
|
|
@@ -98,10 +111,15 @@ async function runFresh(share: Share): Promise<void> {
|
|
|
98
111
|
try {
|
|
99
112
|
const data = await load(abort.signal);
|
|
100
113
|
if (ticket !== share.generation || abort.signal.aborted) return;
|
|
114
|
+
share.halted = false;
|
|
101
115
|
share.snapshot = { data, loading: false, error: null };
|
|
102
116
|
emit(share);
|
|
103
117
|
} catch (cause) {
|
|
104
118
|
if (ticket !== share.generation || abort.signal.aborted) return;
|
|
119
|
+
if (isMachinesAccessRefusal(cause)) {
|
|
120
|
+
share.halted = true;
|
|
121
|
+
stopTimer(share);
|
|
122
|
+
}
|
|
105
123
|
share.snapshot = {
|
|
106
124
|
data: share.snapshot.data,
|
|
107
125
|
loading: false,
|
|
@@ -120,7 +138,7 @@ function run(share: Share, force = false): Promise<void> {
|
|
|
120
138
|
let trailing!: Promise<void>;
|
|
121
139
|
trailing = share.inFlight
|
|
122
140
|
.then(async () => {
|
|
123
|
-
if (!effective(share).enabled) return;
|
|
141
|
+
if (!effective(share).enabled || share.halted) return;
|
|
124
142
|
await run(share);
|
|
125
143
|
})
|
|
126
144
|
.finally(() => {
|
|
@@ -145,12 +163,21 @@ function reconcile(share: Share): void {
|
|
|
145
163
|
share.abort = null;
|
|
146
164
|
share.inFlight = null;
|
|
147
165
|
stopTimer(share);
|
|
166
|
+
if (share.halted) {
|
|
167
|
+
// Disabling is the host's permission/workspace signal. Forget the
|
|
168
|
+
// refusal so a later re-enable reads once immediately.
|
|
169
|
+
share.halted = false;
|
|
170
|
+
share.snapshot = { data: share.snapshot.data, loading: false, error: null };
|
|
171
|
+
emit(share);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
148
174
|
if (share.snapshot.loading) {
|
|
149
175
|
share.snapshot = { ...share.snapshot, loading: false };
|
|
150
176
|
emit(share);
|
|
151
177
|
}
|
|
152
178
|
return;
|
|
153
179
|
}
|
|
180
|
+
if (share.halted) return;
|
|
154
181
|
if (share.snapshot.data !== null || share.snapshot.error) {
|
|
155
182
|
arm(share);
|
|
156
183
|
return;
|
|
@@ -175,6 +202,7 @@ function getOrCreate(shareKey: string): Share {
|
|
|
175
202
|
inFlight: null,
|
|
176
203
|
trailing: null,
|
|
177
204
|
generation: 0,
|
|
205
|
+
halted: false,
|
|
178
206
|
};
|
|
179
207
|
shares.set(shareKey, created);
|
|
180
208
|
return created;
|
|
@@ -183,7 +211,12 @@ function getOrCreate(shareKey: string): Share {
|
|
|
183
211
|
export async function refreshSharedMachinesList(shareKey: string): Promise<void> {
|
|
184
212
|
const share = shares.get(shareKey);
|
|
185
213
|
if (!share) return;
|
|
214
|
+
// An explicit refresh (user action, post-mutation reconcile) may retry once;
|
|
215
|
+
// a repeated refusal halts polling again.
|
|
216
|
+
const wasHalted = share.halted;
|
|
217
|
+
share.halted = false;
|
|
186
218
|
await run(share, true);
|
|
219
|
+
if (wasHalted) arm(share);
|
|
187
220
|
}
|
|
188
221
|
|
|
189
222
|
export function useSharedMachinesList(
|
|
@@ -39,6 +39,12 @@ export type ClientConfigFlags = {
|
|
|
39
39
|
* it off; availability otherwise comes from the realtime model catalog.
|
|
40
40
|
*/
|
|
41
41
|
realtimeVoice: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The host explicitly offers live voice to end users (a session proxy with
|
|
44
|
+
* `realtimeVoice: true`). Embedded conversations show the voice button only
|
|
45
|
+
* then or when their `realtimeVoice` prop is `true`.
|
|
46
|
+
*/
|
|
47
|
+
realtimeVoiceOffered: boolean;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
const INITIAL_FLAGS: ClientConfigFlags = {
|
|
@@ -52,6 +58,7 @@ const INITIAL_FLAGS: ClientConfigFlags = {
|
|
|
52
58
|
archive: true,
|
|
53
59
|
voiceInput: null,
|
|
54
60
|
realtimeVoice: false,
|
|
61
|
+
realtimeVoiceOffered: false,
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
type ClientConfigFlagsInput = Awaited<ReturnType<ClientConfigFlagsSource["getClientConfig"]>>;
|
|
@@ -97,6 +104,7 @@ export function useClientConfigFlags(
|
|
|
97
104
|
archive: config.archive !== false,
|
|
98
105
|
voiceInput: config.voiceInput?.available === true ? config.voiceInput : null,
|
|
99
106
|
realtimeVoice: config.realtimeVoice !== false,
|
|
107
|
+
realtimeVoiceOffered: config.realtimeVoice === true,
|
|
100
108
|
});
|
|
101
109
|
}
|
|
102
110
|
},
|
package/src/hooks/use-goal.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenGeniApiError, type SessionEvent, type SessionGoal } from "@opengeni/sdk";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { useEmbeddedGoal, type EmbeddedGoalClientOverride } from "../session-context";
|
|
4
4
|
import { normalizeError } from "../lib/error-message";
|
|
5
5
|
import {
|
|
@@ -65,7 +65,8 @@ export type UseGoalResult = {
|
|
|
65
65
|
/**
|
|
66
66
|
* The session's goal: state, the autonomy counters (`autoContinuations`,
|
|
67
67
|
* `noProgressStreak`), and pause/resume control. A goal-less session yields
|
|
68
|
-
* `goal: null
|
|
68
|
+
* `goal: null`: read through `findGoal` as a successful null when the client
|
|
69
|
+
* has it, otherwise from an absorbed 404. Live-updates on goal, turn, session,
|
|
69
70
|
* control, and system-update events — pass `options.events` from
|
|
70
71
|
* `useSessionEvents` to reuse its stream.
|
|
71
72
|
*/
|
|
@@ -84,10 +85,19 @@ export function useGoal(
|
|
|
84
85
|
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
85
86
|
const generation = useRef(0);
|
|
86
87
|
const targetKeyRef = useRef<string | null>(null);
|
|
87
|
-
// True after the server answered
|
|
88
|
-
// so turn/session traffic must not refetch
|
|
88
|
+
// True after the server answered that no goal exists. Only a goal event can
|
|
89
|
+
// create a goal, so turn/session traffic must not refetch while none exists.
|
|
89
90
|
const goalAbsentRef = useRef(false);
|
|
91
|
+
// Refresh events not yet covered by a read: "goal" when any was a goal.*
|
|
92
|
+
// event. Events that arrive while a read is in flight are decided when it
|
|
93
|
+
// settles, so the stream's opening burst never re-reads a goal-less session.
|
|
94
|
+
const pendingRefreshRef = useRef<"none" | "refresh" | "goal">("none");
|
|
95
|
+
const sharedEventsRef = useRef(sharedEvents);
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
sharedEventsRef.current = sharedEvents;
|
|
98
|
+
}, [sharedEvents]);
|
|
90
99
|
const loadAbort = useRef<AbortController | null>(null);
|
|
100
|
+
const scheduleRefreshRef = useRef<() => void>(() => undefined);
|
|
91
101
|
|
|
92
102
|
const retireLoads = useCallback(() => {
|
|
93
103
|
generation.current += 1;
|
|
@@ -100,15 +110,20 @@ export function useGoal(
|
|
|
100
110
|
return;
|
|
101
111
|
}
|
|
102
112
|
const ticket = ++generation.current;
|
|
113
|
+
pendingRefreshRef.current = "none";
|
|
103
114
|
loadAbort.current?.abort();
|
|
104
115
|
const controller = new AbortController();
|
|
105
116
|
loadAbort.current = controller;
|
|
106
117
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
// findGoal reads a goal-less session as a successful null, so no failed
|
|
119
|
+
// request reaches the browser console. Custom clients without it keep
|
|
120
|
+
// getGoal, whose 404 is absorbed below (as it is from older servers).
|
|
121
|
+
const fetched =
|
|
122
|
+
typeof client.findGoal === "function"
|
|
123
|
+
? await client.findGoal(workspaceId, sessionId, { signal: controller.signal })
|
|
124
|
+
: await client.getGoal(workspaceId, sessionId, { signal: controller.signal });
|
|
110
125
|
if (ticket === generation.current) {
|
|
111
|
-
goalAbsentRef.current =
|
|
126
|
+
goalAbsentRef.current = fetched === null;
|
|
112
127
|
setGoal(fetched);
|
|
113
128
|
setError(null);
|
|
114
129
|
setLoading(false);
|
|
@@ -129,6 +144,26 @@ export function useGoal(
|
|
|
129
144
|
} finally {
|
|
130
145
|
if (loadAbort.current === controller) loadAbort.current = null;
|
|
131
146
|
}
|
|
147
|
+
if (ticket !== generation.current || pendingRefreshRef.current === "none") {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Events arrived during this read. A shared feed's first batch reports only
|
|
151
|
+
// its latest match, so a goal.* event behind newer turn traffic is visible
|
|
152
|
+
// only in the log: re-read when its latest goal event is not a clear.
|
|
153
|
+
if (goalAbsentRef.current && pendingRefreshRef.current === "refresh") {
|
|
154
|
+
const events = sharedEventsRef.current ?? [];
|
|
155
|
+
let latestGoalEvent: SessionEvent | undefined;
|
|
156
|
+
for (let index = events.length - 1; index >= 0 && !latestGoalEvent; index -= 1) {
|
|
157
|
+
const event = events[index];
|
|
158
|
+
if (event && isGoalEvent(event)) latestGoalEvent = event;
|
|
159
|
+
}
|
|
160
|
+
if (!latestGoalEvent || latestGoalEvent.type === "goal.cleared") {
|
|
161
|
+
pendingRefreshRef.current = "none";
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
pendingRefreshRef.current = "goal";
|
|
165
|
+
}
|
|
166
|
+
scheduleRefreshRef.current();
|
|
132
167
|
}, [client, workspaceId, sessionId]);
|
|
133
168
|
|
|
134
169
|
useEffect(() => {
|
|
@@ -136,6 +171,7 @@ export function useGoal(
|
|
|
136
171
|
if (targetKeyRef.current !== targetKey) {
|
|
137
172
|
targetKeyRef.current = targetKey;
|
|
138
173
|
goalAbsentRef.current = false;
|
|
174
|
+
pendingRefreshRef.current = "none";
|
|
139
175
|
setGoal(null);
|
|
140
176
|
setError(null);
|
|
141
177
|
}
|
|
@@ -186,13 +222,33 @@ export function useGoal(
|
|
|
186
222
|
sharedFeed,
|
|
187
223
|
]);
|
|
188
224
|
|
|
189
|
-
const scheduleRefresh = useDebouncedCallback(() =>
|
|
225
|
+
const scheduleRefresh = useDebouncedCallback(() => {
|
|
226
|
+
const pending = pendingRefreshRef.current;
|
|
227
|
+
// An in-flight read settles pending events itself.
|
|
228
|
+
if (pending === "none" || loadAbort.current !== null) return;
|
|
229
|
+
if (pending === "refresh" && goalAbsentRef.current) {
|
|
230
|
+
pendingRefreshRef.current = "none";
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
void load();
|
|
234
|
+
});
|
|
235
|
+
useLayoutEffect(() => {
|
|
236
|
+
scheduleRefreshRef.current = scheduleRefresh;
|
|
237
|
+
}, [scheduleRefresh]);
|
|
190
238
|
const isRefreshEvent = useCallback(
|
|
191
239
|
(event: SessionEvent) =>
|
|
192
240
|
goalAbsentRef.current ? isGoalEvent(event) : isGoalRefreshEvent(event),
|
|
193
241
|
[],
|
|
194
242
|
);
|
|
195
|
-
|
|
243
|
+
const onRefreshEvent = useCallback(
|
|
244
|
+
(event: SessionEvent) => {
|
|
245
|
+
if (isGoalEvent(event)) pendingRefreshRef.current = "goal";
|
|
246
|
+
else if (pendingRefreshRef.current === "none") pendingRefreshRef.current = "refresh";
|
|
247
|
+
scheduleRefresh();
|
|
248
|
+
},
|
|
249
|
+
[scheduleRefresh],
|
|
250
|
+
);
|
|
251
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isRefreshEvent, onRefreshEvent, {
|
|
196
252
|
enabled,
|
|
197
253
|
...(sharedEvents !== undefined ? { events: sharedEvents } : {}),
|
|
198
254
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { HumanInputAnswer, HumanInputQuestion } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
/* Pure drafting and validation rules for structured human-input requests,
|
|
4
|
+
shared by the web HumanInputForm and non-DOM renderers. */
|
|
5
|
+
|
|
6
|
+
export type HumanInputAnswerDraft = {
|
|
7
|
+
values: string[];
|
|
8
|
+
other: string;
|
|
9
|
+
otherSelected: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type HumanInputFormMessages = {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
submit: string;
|
|
16
|
+
skip: string;
|
|
17
|
+
submitting: string;
|
|
18
|
+
other: string;
|
|
19
|
+
deadlineLabel: string;
|
|
20
|
+
formatDeadline: (value: string) => string;
|
|
21
|
+
required: string;
|
|
22
|
+
otherRequired: string;
|
|
23
|
+
minSelections: (count: number) => string;
|
|
24
|
+
maxSelections: (count: number) => string;
|
|
25
|
+
optional: string;
|
|
26
|
+
/** Shown when the question list overflows the card and more content is below. */
|
|
27
|
+
moreBelow: string;
|
|
28
|
+
questionCount: (count: number) => string;
|
|
29
|
+
collapse: string;
|
|
30
|
+
expand: string;
|
|
31
|
+
selectionHint: (min: number | null | undefined, max: number | null | undefined) => string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const defaultHumanInputFormMessages: HumanInputFormMessages = {
|
|
35
|
+
title: "Input required",
|
|
36
|
+
/** Multi-question chrome has no default subtitle; hosts may still override. */
|
|
37
|
+
description: "",
|
|
38
|
+
submit: "Send answers",
|
|
39
|
+
skip: "Skip",
|
|
40
|
+
submitting: "Submitting…",
|
|
41
|
+
other: "Other",
|
|
42
|
+
deadlineLabel: "Expires",
|
|
43
|
+
formatDeadline,
|
|
44
|
+
required: "This question is required.",
|
|
45
|
+
otherRequired: "Enter a value for Other.",
|
|
46
|
+
minSelections: (count) => `Choose at least ${count} option${count === 1 ? "" : "s"}.`,
|
|
47
|
+
maxSelections: (count) => `Choose no more than ${count} option${count === 1 ? "" : "s"}.`,
|
|
48
|
+
optional: "Optional",
|
|
49
|
+
moreBelow: "More below",
|
|
50
|
+
questionCount: (count) => `${count} questions`,
|
|
51
|
+
collapse: "Collapse",
|
|
52
|
+
expand: "Expand",
|
|
53
|
+
selectionHint: (min, max) => {
|
|
54
|
+
if (min != null && max != null) return `Choose ${min}–${max}.`;
|
|
55
|
+
if (min != null) return `Choose at least ${min}.`;
|
|
56
|
+
if (max != null) return `Choose up to ${max}.`;
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function answersFromDrafts(
|
|
62
|
+
questions: HumanInputQuestion[],
|
|
63
|
+
drafts: Record<string, HumanInputAnswerDraft>,
|
|
64
|
+
messageOverrides: Partial<HumanInputFormMessages> = {},
|
|
65
|
+
): { answers: HumanInputAnswer[]; errors: Record<string, string> } {
|
|
66
|
+
const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
|
|
67
|
+
const answers: HumanInputAnswer[] = [];
|
|
68
|
+
const errors: Record<string, string> = {};
|
|
69
|
+
for (const question of questions) {
|
|
70
|
+
const draft = drafts[question.id] ?? emptyDraft();
|
|
71
|
+
const values = question.kind === "text" ? draft.values.filter(Boolean) : draft.values;
|
|
72
|
+
const other = draft.otherSelected ? draft.other : "";
|
|
73
|
+
const hasOther = Boolean(other.trim());
|
|
74
|
+
const supplied = values.length + (hasOther ? 1 : 0);
|
|
75
|
+
|
|
76
|
+
// Other-selected-but-empty must win over generic "required" — otherwise the
|
|
77
|
+
// user sees the wrong diagnosis next to a clearly selected control.
|
|
78
|
+
if (question.kind !== "text" && draft.otherSelected && !hasOther) {
|
|
79
|
+
errors[question.id] = messages.otherRequired;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (question.required && supplied === 0) {
|
|
84
|
+
errors[question.id] = messages.required;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (question.kind !== "text") {
|
|
88
|
+
const min = question.validation?.minSelections;
|
|
89
|
+
const max = question.kind === "single_select" ? 1 : question.validation?.maxSelections;
|
|
90
|
+
if (min != null && supplied < min) {
|
|
91
|
+
errors[question.id] = messages.minSelections(min);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (max != null && supplied > max) {
|
|
95
|
+
errors[question.id] = messages.maxSelections(max);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (supplied > 0) {
|
|
100
|
+
answers.push({
|
|
101
|
+
questionId: question.id,
|
|
102
|
+
values,
|
|
103
|
+
...(hasOther ? { other } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { answers, errors };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function initialDrafts(
|
|
111
|
+
questions: HumanInputQuestion[],
|
|
112
|
+
): Record<string, HumanInputAnswerDraft> {
|
|
113
|
+
return Object.fromEntries(questions.map((question) => [question.id, emptyDraft()]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function emptyDraft(): HumanInputAnswerDraft {
|
|
117
|
+
return { values: [], other: "", otherSelected: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatDeadline(value: string): string {
|
|
121
|
+
const date = new Date(value);
|
|
122
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
123
|
+
const ms = date.getTime() - Date.now();
|
|
124
|
+
if (ms <= 0) return "deadline passed";
|
|
125
|
+
const minutes = Math.round(ms / 60_000);
|
|
126
|
+
if (minutes < 1) return "in under a minute";
|
|
127
|
+
if (minutes < 60) return `in ${minutes}m`;
|
|
128
|
+
const hours = Math.round(minutes / 60);
|
|
129
|
+
if (hours < 48) return `in ${hours}h`;
|
|
130
|
+
return date.toLocaleString();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The form heading: a single question leads with its own label/prompt. */
|
|
134
|
+
export function humanInputHeading(
|
|
135
|
+
questions: readonly HumanInputQuestion[],
|
|
136
|
+
messages: Pick<HumanInputFormMessages, "title" | "description">,
|
|
137
|
+
): { title: string; description: string | null } {
|
|
138
|
+
const single = questions.length === 1 ? questions[0]! : null;
|
|
139
|
+
return {
|
|
140
|
+
title: single ? (single.label ?? single.prompt) : messages.title,
|
|
141
|
+
description: single
|
|
142
|
+
? single.label
|
|
143
|
+
? single.prompt
|
|
144
|
+
: (single.helpText ?? null)
|
|
145
|
+
: messages.description || null,
|
|
146
|
+
};
|
|
147
|
+
}
|