@opengeni/react 0.15.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +85 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HumanInputAnswer,
|
|
3
|
+
HumanInputQuestion,
|
|
4
|
+
SessionHumanInputRequest,
|
|
5
|
+
SubmitHumanInputResponseRequest,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
7
|
+
import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
|
|
8
|
+
import { cn } from "../lib/cn";
|
|
9
|
+
|
|
10
|
+
export type HumanInputAnswerDraft = {
|
|
11
|
+
values: string[];
|
|
12
|
+
other: string;
|
|
13
|
+
otherSelected: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type HumanInputFormMessages = {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
submit: string;
|
|
20
|
+
skip: string;
|
|
21
|
+
submitting: string;
|
|
22
|
+
other: string;
|
|
23
|
+
deadlineLabel: string;
|
|
24
|
+
formatDeadline: (value: string) => string;
|
|
25
|
+
required: string;
|
|
26
|
+
minLength: (count: number) => string;
|
|
27
|
+
maxLength: (count: number) => string;
|
|
28
|
+
otherRequired: string;
|
|
29
|
+
minSelections: (count: number) => string;
|
|
30
|
+
maxSelections: (count: number) => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const defaultHumanInputFormMessages: HumanInputFormMessages = {
|
|
34
|
+
title: "Your input is needed",
|
|
35
|
+
description: "The agent is paused until you answer these questions.",
|
|
36
|
+
submit: "Continue",
|
|
37
|
+
skip: "Skip",
|
|
38
|
+
submitting: "Submitting…",
|
|
39
|
+
other: "Other",
|
|
40
|
+
deadlineLabel: "Respond before",
|
|
41
|
+
formatDeadline,
|
|
42
|
+
required: "This question is required.",
|
|
43
|
+
minLength: (count) => `Enter at least ${count} characters.`,
|
|
44
|
+
maxLength: (count) => `Enter no more than ${count} characters.`,
|
|
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
|
+
};
|
|
49
|
+
|
|
50
|
+
export type HumanInputFormProps = {
|
|
51
|
+
request: Pick<SessionHumanInputRequest, "id" | "questions" | "allowSkip" | "expiresAt">;
|
|
52
|
+
onSubmit: (response: SubmitHumanInputResponseRequest) => void | Promise<void>;
|
|
53
|
+
submitting?: boolean | undefined;
|
|
54
|
+
error?: string | null | undefined;
|
|
55
|
+
title?: ReactNode;
|
|
56
|
+
description?: ReactNode;
|
|
57
|
+
submitLabel?: string | undefined;
|
|
58
|
+
skipLabel?: string | undefined;
|
|
59
|
+
messages?: Partial<HumanInputFormMessages> | undefined;
|
|
60
|
+
autoFocus?: boolean | undefined;
|
|
61
|
+
className?: string | undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Styled but host-neutral renderer for one structured request. Hosts can use
|
|
66
|
+
* the headless `useHumanInputRequests` hook instead, or replace the title and
|
|
67
|
+
* description while retaining accessible field semantics and validation.
|
|
68
|
+
*/
|
|
69
|
+
export function HumanInputForm({
|
|
70
|
+
request,
|
|
71
|
+
onSubmit,
|
|
72
|
+
submitting = false,
|
|
73
|
+
error,
|
|
74
|
+
title,
|
|
75
|
+
description,
|
|
76
|
+
submitLabel,
|
|
77
|
+
skipLabel,
|
|
78
|
+
messages: messageOverrides,
|
|
79
|
+
autoFocus = true,
|
|
80
|
+
className,
|
|
81
|
+
}: HumanInputFormProps) {
|
|
82
|
+
const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
|
|
83
|
+
const resolvedTitle = title === undefined ? messages.title : title;
|
|
84
|
+
const resolvedDescription = description === undefined ? messages.description : description;
|
|
85
|
+
const resolvedSubmitLabel = submitLabel ?? messages.submit;
|
|
86
|
+
const resolvedSkipLabel = skipLabel ?? messages.skip;
|
|
87
|
+
const formId = useId();
|
|
88
|
+
const [drafts, setDrafts] = useState<Record<string, HumanInputAnswerDraft>>(() =>
|
|
89
|
+
initialDrafts(request.questions),
|
|
90
|
+
);
|
|
91
|
+
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
|
92
|
+
const [submissionError, setSubmissionError] = useState<string | null>(null);
|
|
93
|
+
const [submittingInternally, setSubmittingInternally] = useState(false);
|
|
94
|
+
const submissionInFlight = useRef(false);
|
|
95
|
+
const submissionGeneration = useRef(0);
|
|
96
|
+
const busy = submitting || submittingInternally;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
submissionGeneration.current += 1;
|
|
100
|
+
submissionInFlight.current = false;
|
|
101
|
+
setDrafts(initialDrafts(request.questions));
|
|
102
|
+
setValidationErrors({});
|
|
103
|
+
setSubmissionError(null);
|
|
104
|
+
setSubmittingInternally(false);
|
|
105
|
+
}, [request.id, request.questions]);
|
|
106
|
+
|
|
107
|
+
const update = (
|
|
108
|
+
questionId: string,
|
|
109
|
+
apply: (draft: HumanInputAnswerDraft) => HumanInputAnswerDraft,
|
|
110
|
+
): void => {
|
|
111
|
+
setDrafts((current) => ({
|
|
112
|
+
...current,
|
|
113
|
+
[questionId]: apply(current[questionId] ?? emptyDraft()),
|
|
114
|
+
}));
|
|
115
|
+
setValidationErrors((current) => {
|
|
116
|
+
if (!(questionId in current)) return current;
|
|
117
|
+
const next = { ...current };
|
|
118
|
+
delete next[questionId];
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const submitResponse = async (response: SubmitHumanInputResponseRequest): Promise<void> => {
|
|
124
|
+
if (busy || submissionInFlight.current) return;
|
|
125
|
+
const generation = submissionGeneration.current;
|
|
126
|
+
submissionInFlight.current = true;
|
|
127
|
+
setSubmissionError(null);
|
|
128
|
+
setSubmittingInternally(true);
|
|
129
|
+
try {
|
|
130
|
+
await onSubmit(response);
|
|
131
|
+
} catch (cause) {
|
|
132
|
+
if (generation === submissionGeneration.current) {
|
|
133
|
+
setSubmissionError(cause instanceof Error ? cause.message : String(cause));
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
if (generation === submissionGeneration.current) {
|
|
137
|
+
submissionInFlight.current = false;
|
|
138
|
+
setSubmittingInternally(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const result = answersFromDrafts(request.questions, drafts, messages);
|
|
146
|
+
if (Object.keys(result.errors).length > 0) {
|
|
147
|
+
setValidationErrors(result.errors);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await submitResponse({ outcome: "answered", answers: result.answers });
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<form
|
|
155
|
+
data-human-input-request={request.id}
|
|
156
|
+
onSubmit={(event) => void submit(event)}
|
|
157
|
+
className={cn(
|
|
158
|
+
"og-root flex w-full flex-col gap-5 rounded-og-lg border border-og-border bg-og-surface-1 p-5 shadow-og-sm",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<header>
|
|
163
|
+
<h2 className="text-og-md font-semibold text-og-fg">{resolvedTitle}</h2>
|
|
164
|
+
{resolvedDescription ? (
|
|
165
|
+
<div className="mt-1 text-og-sm text-og-fg-muted">{resolvedDescription}</div>
|
|
166
|
+
) : null}
|
|
167
|
+
{request.expiresAt ? (
|
|
168
|
+
<p className="mt-1 text-og-xs text-og-fg-subtle">
|
|
169
|
+
{messages.deadlineLabel}{" "}
|
|
170
|
+
<time dateTime={request.expiresAt}>{messages.formatDeadline(request.expiresAt)}</time>
|
|
171
|
+
</p>
|
|
172
|
+
) : null}
|
|
173
|
+
</header>
|
|
174
|
+
|
|
175
|
+
<fieldset disabled={busy} className="contents">
|
|
176
|
+
{request.questions.map((question, index) => {
|
|
177
|
+
const draft = drafts[question.id] ?? emptyDraft();
|
|
178
|
+
const fieldId = `${formId}-${index}`;
|
|
179
|
+
const errorId = `${fieldId}-error`;
|
|
180
|
+
const helpId = `${fieldId}-help`;
|
|
181
|
+
const describedBy =
|
|
182
|
+
[question.helpText ? helpId : null, validationErrors[question.id] ? errorId : null]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join(" ") || undefined;
|
|
185
|
+
return (
|
|
186
|
+
<div key={question.id} className="flex flex-col gap-2">
|
|
187
|
+
<label
|
|
188
|
+
htmlFor={question.kind === "text" ? fieldId : undefined}
|
|
189
|
+
className="text-og-base font-medium text-og-fg"
|
|
190
|
+
>
|
|
191
|
+
{question.label ?? question.prompt}
|
|
192
|
+
{question.required ? (
|
|
193
|
+
<span aria-hidden className="ml-1 text-og-status-failed">
|
|
194
|
+
*
|
|
195
|
+
</span>
|
|
196
|
+
) : null}
|
|
197
|
+
</label>
|
|
198
|
+
{question.label ? (
|
|
199
|
+
<p className="text-og-sm text-og-fg-muted">{question.prompt}</p>
|
|
200
|
+
) : null}
|
|
201
|
+
{question.helpText ? (
|
|
202
|
+
<p id={helpId} className="text-og-xs text-og-fg-subtle">
|
|
203
|
+
{question.helpText}
|
|
204
|
+
</p>
|
|
205
|
+
) : null}
|
|
206
|
+
{question.kind === "text" ? (
|
|
207
|
+
<textarea
|
|
208
|
+
id={fieldId}
|
|
209
|
+
value={draft.values[0] ?? ""}
|
|
210
|
+
onChange={(event) =>
|
|
211
|
+
update(question.id, (current) => ({
|
|
212
|
+
...current,
|
|
213
|
+
values: event.target.value ? [event.target.value] : [],
|
|
214
|
+
}))
|
|
215
|
+
}
|
|
216
|
+
aria-invalid={Boolean(validationErrors[question.id])}
|
|
217
|
+
aria-describedby={describedBy}
|
|
218
|
+
autoFocus={autoFocus && index === 0}
|
|
219
|
+
rows={3}
|
|
220
|
+
className="min-h-20 w-full resize-y rounded-og-md border border-og-border bg-og-surface-2 px-3 py-2 text-og-base text-og-fg outline-none placeholder:text-og-fg-subtle focus:border-og-accent"
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<div
|
|
224
|
+
role={question.kind === "single_select" ? "radiogroup" : "group"}
|
|
225
|
+
aria-describedby={describedBy}
|
|
226
|
+
className="flex flex-col gap-2"
|
|
227
|
+
>
|
|
228
|
+
{question.options.map((option, optionIndex) => {
|
|
229
|
+
const checked = draft.values.includes(option.id);
|
|
230
|
+
return (
|
|
231
|
+
<label
|
|
232
|
+
key={option.id}
|
|
233
|
+
className="flex cursor-pointer items-start gap-2 rounded-og-md border border-og-border px-3 py-2 hover:bg-og-surface-2"
|
|
234
|
+
>
|
|
235
|
+
<input
|
|
236
|
+
type={question.kind === "single_select" ? "radio" : "checkbox"}
|
|
237
|
+
name={question.kind === "single_select" ? fieldId : undefined}
|
|
238
|
+
autoFocus={autoFocus && index === 0 && optionIndex === 0}
|
|
239
|
+
checked={checked}
|
|
240
|
+
onChange={(event) =>
|
|
241
|
+
update(question.id, (current) => ({
|
|
242
|
+
...current,
|
|
243
|
+
values:
|
|
244
|
+
question.kind === "single_select"
|
|
245
|
+
? event.target.checked
|
|
246
|
+
? [option.id]
|
|
247
|
+
: []
|
|
248
|
+
: event.target.checked
|
|
249
|
+
? [...current.values, option.id]
|
|
250
|
+
: current.values.filter((value) => value !== option.id),
|
|
251
|
+
...(question.kind === "single_select" && event.target.checked
|
|
252
|
+
? { otherSelected: false }
|
|
253
|
+
: {}),
|
|
254
|
+
}))
|
|
255
|
+
}
|
|
256
|
+
className="mt-0.5 accent-og-accent"
|
|
257
|
+
/>
|
|
258
|
+
<span className="min-w-0">
|
|
259
|
+
<span className="block text-og-sm font-medium text-og-fg">
|
|
260
|
+
{option.label}
|
|
261
|
+
</span>
|
|
262
|
+
{option.description ? (
|
|
263
|
+
<span className="block text-og-xs text-og-fg-muted">
|
|
264
|
+
{option.description}
|
|
265
|
+
</span>
|
|
266
|
+
) : null}
|
|
267
|
+
</span>
|
|
268
|
+
</label>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
{question.allowOther ? (
|
|
272
|
+
<label className="flex items-start gap-2 rounded-og-md border border-og-border px-3 py-2">
|
|
273
|
+
<input
|
|
274
|
+
type={question.kind === "single_select" ? "radio" : "checkbox"}
|
|
275
|
+
name={question.kind === "single_select" ? fieldId : undefined}
|
|
276
|
+
checked={draft.otherSelected}
|
|
277
|
+
autoFocus={autoFocus && index === 0 && question.options.length === 0}
|
|
278
|
+
onChange={(event) =>
|
|
279
|
+
update(question.id, (current) => ({
|
|
280
|
+
...current,
|
|
281
|
+
otherSelected: event.target.checked,
|
|
282
|
+
...(question.kind === "single_select" && event.target.checked
|
|
283
|
+
? { values: [] }
|
|
284
|
+
: {}),
|
|
285
|
+
}))
|
|
286
|
+
}
|
|
287
|
+
className="mt-2 accent-og-accent"
|
|
288
|
+
/>
|
|
289
|
+
<span className="min-w-0 flex-1">
|
|
290
|
+
<span className="block text-og-sm font-medium text-og-fg">
|
|
291
|
+
{messages.other}
|
|
292
|
+
</span>
|
|
293
|
+
<input
|
|
294
|
+
type="text"
|
|
295
|
+
value={draft.other}
|
|
296
|
+
disabled={!draft.otherSelected || busy}
|
|
297
|
+
onChange={(event) =>
|
|
298
|
+
update(question.id, (current) => ({
|
|
299
|
+
...current,
|
|
300
|
+
other: event.target.value,
|
|
301
|
+
}))
|
|
302
|
+
}
|
|
303
|
+
className="mt-1 w-full rounded-og-sm border border-og-border bg-og-surface-2 px-2 py-1.5 text-og-sm text-og-fg outline-none focus:border-og-accent disabled:opacity-50"
|
|
304
|
+
/>
|
|
305
|
+
</span>
|
|
306
|
+
</label>
|
|
307
|
+
) : null}
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
{validationErrors[question.id] ? (
|
|
311
|
+
<p id={errorId} role="alert" className="text-og-xs text-og-status-failed">
|
|
312
|
+
{validationErrors[question.id]}
|
|
313
|
+
</p>
|
|
314
|
+
) : null}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
</fieldset>
|
|
319
|
+
|
|
320
|
+
{(error ?? submissionError) ? (
|
|
321
|
+
<p role="alert" className="text-og-sm text-og-status-failed">
|
|
322
|
+
{error ?? submissionError}
|
|
323
|
+
</p>
|
|
324
|
+
) : null}
|
|
325
|
+
<footer className="flex items-center justify-end gap-2">
|
|
326
|
+
{request.allowSkip ? (
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
disabled={busy}
|
|
330
|
+
onClick={() => void submitResponse({ outcome: "skipped" })}
|
|
331
|
+
className="rounded-og-md border border-og-border px-3 py-2 text-og-sm font-medium text-og-fg-muted hover:bg-og-surface-2 disabled:opacity-50"
|
|
332
|
+
>
|
|
333
|
+
{resolvedSkipLabel}
|
|
334
|
+
</button>
|
|
335
|
+
) : null}
|
|
336
|
+
<button
|
|
337
|
+
type="submit"
|
|
338
|
+
disabled={busy}
|
|
339
|
+
className="rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-accent-fg hover:bg-og-accent-strong disabled:opacity-50"
|
|
340
|
+
>
|
|
341
|
+
{busy ? messages.submitting : resolvedSubmitLabel}
|
|
342
|
+
</button>
|
|
343
|
+
</footer>
|
|
344
|
+
</form>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function answersFromDrafts(
|
|
349
|
+
questions: HumanInputQuestion[],
|
|
350
|
+
drafts: Record<string, HumanInputAnswerDraft>,
|
|
351
|
+
messageOverrides: Partial<HumanInputFormMessages> = {},
|
|
352
|
+
): { answers: HumanInputAnswer[]; errors: Record<string, string> } {
|
|
353
|
+
const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
|
|
354
|
+
const answers: HumanInputAnswer[] = [];
|
|
355
|
+
const errors: Record<string, string> = {};
|
|
356
|
+
for (const question of questions) {
|
|
357
|
+
const draft = drafts[question.id] ?? emptyDraft();
|
|
358
|
+
const values = question.kind === "text" ? draft.values.filter(Boolean) : draft.values;
|
|
359
|
+
const other = draft.otherSelected ? draft.other.trim() : "";
|
|
360
|
+
const supplied = values.length + (other ? 1 : 0);
|
|
361
|
+
if (question.required && supplied === 0) {
|
|
362
|
+
errors[question.id] = messages.required;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (question.kind === "text") {
|
|
366
|
+
const value = values[0] ?? "";
|
|
367
|
+
if (
|
|
368
|
+
value &&
|
|
369
|
+
question.validation?.minLength != null &&
|
|
370
|
+
value.length < question.validation.minLength
|
|
371
|
+
) {
|
|
372
|
+
errors[question.id] = messages.minLength(question.validation.minLength);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (question.validation?.maxLength != null && value.length > question.validation.maxLength) {
|
|
376
|
+
errors[question.id] = messages.maxLength(question.validation.maxLength);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
if (draft.otherSelected && !other) {
|
|
381
|
+
errors[question.id] = messages.otherRequired;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const min = question.validation?.minSelections;
|
|
385
|
+
const max = question.kind === "single_select" ? 1 : question.validation?.maxSelections;
|
|
386
|
+
if (min != null && supplied < min) {
|
|
387
|
+
errors[question.id] = messages.minSelections(min);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (max != null && supplied > max) {
|
|
391
|
+
errors[question.id] = messages.maxSelections(max);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (supplied > 0) {
|
|
396
|
+
answers.push({
|
|
397
|
+
questionId: question.id,
|
|
398
|
+
values,
|
|
399
|
+
...(other ? { other } : {}),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { answers, errors };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function initialDrafts(questions: HumanInputQuestion[]): Record<string, HumanInputAnswerDraft> {
|
|
407
|
+
return Object.fromEntries(questions.map((question) => [question.id, emptyDraft()]));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function emptyDraft(): HumanInputAnswerDraft {
|
|
411
|
+
return { values: [], other: "", otherSelected: false };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function formatDeadline(value: string): string {
|
|
415
|
+
const date = new Date(value);
|
|
416
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
417
|
+
}
|
|
@@ -194,7 +194,7 @@ export function MachineCard({
|
|
|
194
194
|
onAttach?.(machine);
|
|
195
195
|
}}
|
|
196
196
|
className={cn(
|
|
197
|
-
"rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-
|
|
197
|
+
"rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-11",
|
|
198
198
|
"hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50",
|
|
199
199
|
)}
|
|
200
200
|
>
|
|
@@ -46,7 +46,7 @@ function EmptyState({ onEnroll }: { onEnroll?: (() => void) | undefined }) {
|
|
|
46
46
|
type="button"
|
|
47
47
|
data-enroll-cta
|
|
48
48
|
onClick={onEnroll}
|
|
49
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-
|
|
49
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-11"
|
|
50
50
|
>
|
|
51
51
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
52
52
|
Enroll a machine
|
|
@@ -90,7 +90,7 @@ function Header({
|
|
|
90
90
|
type="button"
|
|
91
91
|
data-enroll-cta
|
|
92
92
|
onClick={onEnroll}
|
|
93
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-
|
|
93
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-11"
|
|
94
94
|
>
|
|
95
95
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
96
96
|
Enroll machine
|
|
@@ -140,7 +140,7 @@ export function MachinesDashboard({
|
|
|
140
140
|
data-machines-retry
|
|
141
141
|
onClick={onRefresh}
|
|
142
142
|
disabled={loading}
|
|
143
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-
|
|
143
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-11"
|
|
144
144
|
>
|
|
145
145
|
<RefreshCwIcon className={cn("size-3.5", loading && "animate-og-spin")} aria-hidden />
|
|
146
146
|
{loading ? "Retrying…" : "Retry"}
|