@agent-native/core 0.120.0 → 0.120.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
- package/corpus/core/src/client/use-chat-models.ts +45 -0
- package/corpus/core/src/server/analytics.ts +15 -0
- package/corpus/core/src/server/auth.ts +17 -13
- package/corpus/core/src/server/better-auth-instance.ts +13 -1
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
- package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
- package/corpus/templates/forms/AGENTS.md +11 -1
- package/corpus/templates/forms/README.md +24 -1
- package/corpus/templates/forms/actions/update-form.ts +12 -5
- package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
- package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
- package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
- package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
- package/corpus/templates/forms/server/lib/integrations.ts +11 -2
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
- package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/shared/conditional.ts +67 -0
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +35 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/use-chat-models.d.ts +1 -0
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +33 -0
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +9 -9
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/server/analytics.d.ts +8 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +16 -0
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -4
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +8 -0
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +10 -1
- package/dist/server/better-auth-instance.js.map +1 -1
- package/package.json +1 -1
- package/src/client/MultiTabAssistantChat.tsx +45 -0
- package/src/client/use-chat-models.ts +45 -0
- package/src/server/analytics.ts +15 -0
- package/src/server/auth.ts +17 -13
- package/src/server/better-auth-instance.ts +13 -1
|
@@ -295,6 +295,18 @@ export function FormBuilderPage() {
|
|
|
295
295
|
[updateForm],
|
|
296
296
|
);
|
|
297
297
|
|
|
298
|
+
const saveImmediately = useCallback(
|
|
299
|
+
async (data: Parameters<typeof updateForm.mutate>[0]) => {
|
|
300
|
+
clearTimeout(saveTimeout.current);
|
|
301
|
+
try {
|
|
302
|
+
await updateForm.mutateAsync(data);
|
|
303
|
+
} finally {
|
|
304
|
+
fieldsDirty.current = false;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[updateForm],
|
|
308
|
+
);
|
|
309
|
+
|
|
298
310
|
// Debounced field-op save — uses patch-form-fields (server-side merge) so
|
|
299
311
|
// concurrent edits to different fields both survive.
|
|
300
312
|
const fieldOpTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
@@ -853,8 +865,17 @@ export function FormBuilderPage() {
|
|
|
853
865
|
key={JSON.stringify(form.settings)}
|
|
854
866
|
form={form}
|
|
855
867
|
onSave={(settings) => {
|
|
856
|
-
|
|
857
|
-
|
|
868
|
+
void saveImmediately({ id: form.id, settings })
|
|
869
|
+
.then(() => toast.success(t("builder.settingsSaved")))
|
|
870
|
+
.catch((error: unknown) => {
|
|
871
|
+
toast.error(
|
|
872
|
+
error instanceof Error && error.message
|
|
873
|
+
? error.message
|
|
874
|
+
: t("builder.saveFailed", {
|
|
875
|
+
defaultValue: "Failed to save changes",
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
});
|
|
858
879
|
}}
|
|
859
880
|
/>
|
|
860
881
|
</div>
|
|
@@ -868,8 +889,17 @@ export function FormBuilderPage() {
|
|
|
868
889
|
key={JSON.stringify(form.settings?.integrations)}
|
|
869
890
|
form={form}
|
|
870
891
|
onSave={(settings) => {
|
|
871
|
-
|
|
872
|
-
|
|
892
|
+
void saveImmediately({ id: form.id, settings })
|
|
893
|
+
.then(() => toast.success(t("builder.integrationsSaved")))
|
|
894
|
+
.catch((error: unknown) => {
|
|
895
|
+
toast.error(
|
|
896
|
+
error instanceof Error && error.message
|
|
897
|
+
? error.message
|
|
898
|
+
: t("builder.saveFailed", {
|
|
899
|
+
defaultValue: "Failed to save changes",
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
});
|
|
873
903
|
}}
|
|
874
904
|
/>
|
|
875
905
|
</div>
|
|
@@ -1054,6 +1084,7 @@ function BuilderContent({
|
|
|
1054
1084
|
>
|
|
1055
1085
|
<FieldPropertiesPanel
|
|
1056
1086
|
field={field}
|
|
1087
|
+
fields={fields}
|
|
1057
1088
|
onChange={onUpdateField}
|
|
1058
1089
|
onDelete={() => onDeleteField(field.id)}
|
|
1059
1090
|
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
import { Turnstile, PoweredByBadge } from "@agent-native/core/client/ui";
|
|
3
|
+
import { isConditionalFieldVisible } from "@shared/conditional";
|
|
3
4
|
import type { FormField, FormSettings } from "@shared/types";
|
|
4
5
|
import { IconCircleCheck, IconRefresh } from "@tabler/icons-react";
|
|
5
6
|
import { useState, useMemo, useEffect } from "react";
|
|
@@ -88,21 +89,7 @@ export function FormFillPage() {
|
|
|
88
89
|
|
|
89
90
|
// Evaluate conditional visibility
|
|
90
91
|
const visibleFields = useMemo(() => {
|
|
91
|
-
return fields.filter((field) =>
|
|
92
|
-
if (!field.conditional) return true;
|
|
93
|
-
const { fieldId, operator, value: condValue } = field.conditional;
|
|
94
|
-
const fieldVal = String(values[fieldId] ?? "");
|
|
95
|
-
switch (operator) {
|
|
96
|
-
case "equals":
|
|
97
|
-
return fieldVal === condValue;
|
|
98
|
-
case "not_equals":
|
|
99
|
-
return fieldVal !== condValue;
|
|
100
|
-
case "contains":
|
|
101
|
-
return fieldVal.includes(condValue);
|
|
102
|
-
default:
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
});
|
|
92
|
+
return fields.filter((field) => isConditionalFieldVisible(field, values));
|
|
106
93
|
}, [fields, values]);
|
|
107
94
|
|
|
108
95
|
function handleChange(fieldId: string, value: unknown) {
|
|
@@ -166,7 +153,11 @@ export function FormFillPage() {
|
|
|
166
153
|
submitForm.mutate(
|
|
167
154
|
{
|
|
168
155
|
formId: form.id,
|
|
169
|
-
data:
|
|
156
|
+
data: Object.fromEntries(
|
|
157
|
+
visibleFields
|
|
158
|
+
.filter((field) => values[field.id] !== undefined)
|
|
159
|
+
.map((field) => [field.id, values[field.id]]),
|
|
160
|
+
),
|
|
170
161
|
captchaToken,
|
|
171
162
|
_hp: honeypot,
|
|
172
163
|
_t: pageLoadTime,
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
} from "h3";
|
|
18
18
|
import { nanoid } from "nanoid";
|
|
19
19
|
|
|
20
|
+
import {
|
|
21
|
+
isConditionalFieldVisible,
|
|
22
|
+
sanitizeConditionalValues as sanitizeVisibleValues,
|
|
23
|
+
} from "../../shared/conditional.js";
|
|
20
24
|
import {
|
|
21
25
|
cleanSubmitterEmail,
|
|
22
26
|
publicSubmitterEmail,
|
|
@@ -158,43 +162,26 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
|
|
|
158
162
|
// Parse form fields and build whitelist of valid field IDs
|
|
159
163
|
const fields: FormField[] = JSON.parse(form.fields);
|
|
160
164
|
const fieldMap = new Map(fields.map((f) => [f.id, f]));
|
|
161
|
-
const
|
|
165
|
+
const submittedData =
|
|
162
166
|
body.data && typeof body.data === "object" && !Array.isArray(body.data)
|
|
163
167
|
? (body.data as Record<string, unknown>)
|
|
164
168
|
: {};
|
|
165
169
|
|
|
166
170
|
// Whitelist: only accept keys matching form field IDs
|
|
167
|
-
const
|
|
168
|
-
for (const [key, value] of Object.entries(
|
|
171
|
+
const whitelistedData: Record<string, unknown> = {};
|
|
172
|
+
for (const [key, value] of Object.entries(submittedData)) {
|
|
169
173
|
const field = fieldMap.get(key);
|
|
170
174
|
if (!field) continue; // Strip unknown fields
|
|
171
|
-
|
|
175
|
+
whitelistedData[key] = value;
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
const data = sanitizeVisibleValues(fields, whitelistedData);
|
|
179
|
+
|
|
174
180
|
// Validate required fields and field-specific constraints. Recompute
|
|
175
181
|
// conditional visibility on the server so direct POSTs cannot submit hidden
|
|
176
182
|
// field values or bypass client-side validation.
|
|
177
|
-
function isFieldVisible(field: FormField): boolean {
|
|
178
|
-
if (!field.conditional) return true;
|
|
179
|
-
const { fieldId, operator, value: condValue } = field.conditional;
|
|
180
|
-
const fieldVal = String(data[fieldId] ?? "");
|
|
181
|
-
switch (operator) {
|
|
182
|
-
case "equals":
|
|
183
|
-
return fieldVal === condValue;
|
|
184
|
-
case "not_equals":
|
|
185
|
-
return fieldVal !== condValue;
|
|
186
|
-
case "contains":
|
|
187
|
-
return fieldVal.includes(condValue);
|
|
188
|
-
default:
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
183
|
for (const field of fields) {
|
|
194
|
-
if (!
|
|
195
|
-
delete data[field.id];
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
184
|
+
if (field.conditional && !isConditionalFieldVisible(field, data)) continue;
|
|
198
185
|
|
|
199
186
|
const val = data[field.id];
|
|
200
187
|
if (field.required && isEmptySubmissionValue(val)) {
|
|
@@ -268,13 +255,17 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
|
|
|
268
255
|
|
|
269
256
|
if (settings.emailOnNewResponses === true && form.ownerEmail) {
|
|
270
257
|
try {
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
258
|
+
await runWithRequestContext(
|
|
259
|
+
{ userEmail: form.ownerEmail, orgId: form.orgId ?? undefined },
|
|
260
|
+
() =>
|
|
261
|
+
sendNewResponseEmail({
|
|
262
|
+
to: form.ownerEmail!,
|
|
263
|
+
formTitle: form.title,
|
|
264
|
+
fields,
|
|
265
|
+
data,
|
|
266
|
+
submittedAt: now,
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
278
269
|
} catch (error) {
|
|
279
270
|
// Email is best-effort — a provider outage must not reject a public
|
|
280
271
|
// submission that was already persisted successfully.
|
|
@@ -118,9 +118,14 @@ function formatFields(
|
|
|
118
118
|
data: Record<string, unknown>,
|
|
119
119
|
): Record<string, unknown> {
|
|
120
120
|
const out: Record<string, unknown> = {};
|
|
121
|
+
const usedLabels = new Set<string>();
|
|
121
122
|
for (const field of fields) {
|
|
122
123
|
if (data[field.id] !== undefined) {
|
|
123
|
-
|
|
124
|
+
const label = field.label.trim() || field.id;
|
|
125
|
+
let key = label;
|
|
126
|
+
if (usedLabels.has(key)) key = `${label} (${field.id})`;
|
|
127
|
+
usedLabels.add(key);
|
|
128
|
+
out[key] = data[field.id];
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
return out;
|
|
@@ -258,9 +263,13 @@ function buildDiscordPayload(submission: SubmissionPayload) {
|
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
/** Google Sheets (Apps Script web app) — flat key/value pairs */
|
|
261
|
-
function buildGoogleSheetsPayload(submission: SubmissionPayload) {
|
|
266
|
+
export function buildGoogleSheetsPayload(submission: SubmissionPayload) {
|
|
262
267
|
return {
|
|
268
|
+
event: "form_submission",
|
|
269
|
+
eventVersion: 1,
|
|
270
|
+
formId: submission.formId,
|
|
263
271
|
formTitle: submission.formTitle,
|
|
272
|
+
responseId: submission.responseId,
|
|
264
273
|
submittedAt: submission.submittedAt,
|
|
265
274
|
submitterEmail: publicSubmitterEmail(submission.submitterEmail) ?? "",
|
|
266
275
|
chatSessionIds: (submission.chatSessionIds ?? []).join(", "),
|
|
@@ -510,19 +510,40 @@ function renderFormPage(
|
|
|
510
510
|
var condVal = el.dataset.condVal;
|
|
511
511
|
var depVal = getFieldValue(depId);
|
|
512
512
|
var show = true;
|
|
513
|
-
if (
|
|
513
|
+
if (Array.isArray(depVal)) {
|
|
514
|
+
if (op === "equals") show = depVal.length === 1 && depVal[0] === condVal;
|
|
515
|
+
else if (op === "not_equals") show = depVal.indexOf(condVal) < 0;
|
|
516
|
+
else if (op === "contains") show = depVal.indexOf(condVal) >= 0;
|
|
517
|
+
} else if (op === "equals") show = depVal === condVal;
|
|
514
518
|
else if (op === "not_equals") show = depVal !== condVal;
|
|
515
519
|
else if (op === "contains") show = depVal.indexOf(condVal) >= 0;
|
|
516
520
|
el.style.display = show ? "" : "none";
|
|
517
521
|
el.dataset.hidden = show ? "" : "1";
|
|
522
|
+
el.querySelectorAll("input, textarea, select, button").forEach(function(control) {
|
|
523
|
+
control.disabled = !show;
|
|
524
|
+
});
|
|
518
525
|
});
|
|
519
526
|
}
|
|
520
527
|
|
|
521
528
|
function getFieldValue(id) {
|
|
522
|
-
var
|
|
523
|
-
if (!
|
|
524
|
-
|
|
525
|
-
|
|
529
|
+
var controls = document.getElementsByName(id);
|
|
530
|
+
if (!controls.length) return "";
|
|
531
|
+
var first = controls[0];
|
|
532
|
+
if (first.type === "checkbox") {
|
|
533
|
+
if (controls.length > 1) {
|
|
534
|
+
return Array.prototype.map.call(controls, function(control) {
|
|
535
|
+
return control.checked ? control.value : "";
|
|
536
|
+
}).filter(Boolean);
|
|
537
|
+
}
|
|
538
|
+
return first.checked ? "true" : "false";
|
|
539
|
+
}
|
|
540
|
+
if (first.type === "radio") {
|
|
541
|
+
for (var i = 0; i < controls.length; i++) {
|
|
542
|
+
if (controls[i].checked) return controls[i].value || "";
|
|
543
|
+
}
|
|
544
|
+
return "";
|
|
545
|
+
}
|
|
546
|
+
return first.value || "";
|
|
526
547
|
}
|
|
527
548
|
|
|
528
549
|
document.getElementById("mainForm").addEventListener("input", updateVisibility);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// runtime — an unrestricted id like `x" onfocus="alert(1)` would otherwise
|
|
5
5
|
// stored-XSS every anonymous submitter of a published form.
|
|
6
6
|
export const FIELD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7
|
+
const CONDITIONAL_OPERATORS = new Set(["equals", "not_equals", "contains"]);
|
|
7
8
|
|
|
8
9
|
export function assertValidFields(fields: unknown): void {
|
|
9
10
|
if (!Array.isArray(fields)) {
|
|
@@ -28,17 +29,30 @@ export function assertValidFields(fields: unknown): void {
|
|
|
28
29
|
seenIds.add(id);
|
|
29
30
|
|
|
30
31
|
const cond = f.conditional;
|
|
31
|
-
if (cond
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
if (cond !== undefined) {
|
|
33
|
+
if (cond == null || typeof cond !== "object") {
|
|
34
|
+
throw new Error(`field #${idx + 1} conditional must be an object`);
|
|
35
|
+
}
|
|
36
|
+
const condition = cond as Record<string, unknown>;
|
|
37
|
+
const condFieldId = condition.fieldId;
|
|
38
|
+
if (
|
|
39
|
+
typeof condFieldId !== "string" ||
|
|
40
|
+
!FIELD_ID_PATTERN.test(condFieldId)
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`field #${idx + 1} conditional.fieldId ${JSON.stringify(condFieldId)} is invalid — must match ${FIELD_ID_PATTERN.source}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
typeof condition.operator !== "string" ||
|
|
48
|
+
!CONDITIONAL_OPERATORS.has(condition.operator)
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`field #${idx + 1} conditional.operator must be equals, not_equals, or contains`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (typeof condition.value !== "string") {
|
|
55
|
+
throw new Error(`field #${idx + 1} conditional.value must be a string`);
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
|
|
@@ -69,4 +83,27 @@ export function assertValidFields(fields: unknown): void {
|
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
}
|
|
86
|
+
|
|
87
|
+
const fieldIndexes = new Map(
|
|
88
|
+
fields.map((field, index) => [
|
|
89
|
+
(field as Record<string, unknown>).id,
|
|
90
|
+
index,
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
for (const [idx, field] of fields.entries()) {
|
|
94
|
+
const condition = (field as Record<string, unknown>).conditional;
|
|
95
|
+
if (!condition || typeof condition !== "object") continue;
|
|
96
|
+
const condFieldId = (condition as Record<string, unknown>).fieldId;
|
|
97
|
+
const sourceIndex = fieldIndexes.get(condFieldId);
|
|
98
|
+
if (sourceIndex === undefined) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`field #${idx + 1} conditional.fieldId ${JSON.stringify(condFieldId)} does not reference a field in this form`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (sourceIndex >= idx) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`field #${idx + 1} conditional.fieldId must reference an earlier field`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
72
109
|
}
|
|
@@ -18,6 +18,7 @@ Core rules:
|
|
|
18
18
|
- When the user asks for an anonymous feedback form or survey, create the complete form atomically with \`status: "published"\`, then return the exact public response URL from the successful action result. Do not return only the private editor link, and do not claim it is live without verifying the saved status and fields.
|
|
19
19
|
- Use \`preview-form\` when the user asks about a form's setup, fields, configuration, publish state, or response count. It renders a native chat summary.
|
|
20
20
|
- When the user wants email notifications for new responses, set \`settings.emailOnNewResponses: true\` with \`create-form\` or \`update-form\`; notifications go to the form owner's account email.
|
|
21
|
+
- When the user wants Slack, Discord, Google Sheets, or webhook notifications, explain that Forms already supports them from the form builder's Integrations tab and navigate there with \`tab: "integrations"\`. Slack uses an Incoming Webhook URL; Google Sheets uses a deployed Apps Script \`/exec\` URL that parses \`e.postData.contents\`. Do not redirect users to the managed Messaging integration for these form destinations.
|
|
21
22
|
- Use \`response-insights\` for response analytics. Do not invent SQL or fake chart data. Pass \`displayMode: "chart"\` for chart-only requests, \`displayMode: "table"\` only when the user asks for a table/rows, and \`displayMode: "insights"\` only for combined dashboards or reports.
|
|
22
23
|
- Use \`list-responses\` and \`export-responses\` for response data review and export. When the user asks to see, open, or view all responses for a form, call \`navigate\` with \`view: "responses"\` instead of listing rows in chat.
|
|
23
24
|
- Use \`navigate\` to open focused workspace views such as the forms list, builder, published preview, responses, response insights, extensions, or team/settings views. For builder sub-tabs, pass \`view=form\`, the form ID, and \`tab=edit|responses|settings|integrations\`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FormField } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function conditionalValue(value: unknown): string {
|
|
4
|
+
if (Array.isArray(value)) return value.map(conditionalValue).join(",");
|
|
5
|
+
if (value === true) return "true";
|
|
6
|
+
if (value === false) return "false";
|
|
7
|
+
if (value == null) return "";
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isConditionalFieldVisible(
|
|
12
|
+
field: Pick<FormField, "conditional">,
|
|
13
|
+
values: Record<string, unknown>,
|
|
14
|
+
): boolean {
|
|
15
|
+
const condition = field.conditional;
|
|
16
|
+
if (!condition) return true;
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
typeof condition.fieldId !== "string" ||
|
|
20
|
+
typeof condition.value !== "string"
|
|
21
|
+
) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rawValue = values[condition.fieldId];
|
|
26
|
+
if (Array.isArray(rawValue)) {
|
|
27
|
+
const selectedValues = rawValue.map(conditionalValue);
|
|
28
|
+
switch (condition.operator) {
|
|
29
|
+
case "equals":
|
|
30
|
+
return (
|
|
31
|
+
selectedValues.length === 1 && selectedValues[0] === condition.value
|
|
32
|
+
);
|
|
33
|
+
case "not_equals":
|
|
34
|
+
return !selectedValues.includes(condition.value);
|
|
35
|
+
case "contains":
|
|
36
|
+
return selectedValues.includes(condition.value);
|
|
37
|
+
default:
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const currentValue = conditionalValue(rawValue);
|
|
43
|
+
switch (condition.operator) {
|
|
44
|
+
case "equals":
|
|
45
|
+
return currentValue === condition.value;
|
|
46
|
+
case "not_equals":
|
|
47
|
+
return currentValue !== condition.value;
|
|
48
|
+
case "contains":
|
|
49
|
+
return currentValue.includes(condition.value);
|
|
50
|
+
default:
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sanitizeConditionalValues(
|
|
56
|
+
fields: FormField[],
|
|
57
|
+
values: Record<string, unknown>,
|
|
58
|
+
): Record<string, unknown> {
|
|
59
|
+
const visibleValues: Record<string, unknown> = {};
|
|
60
|
+
for (const field of fields) {
|
|
61
|
+
if (!isConditionalFieldVisible(field, visibleValues)) continue;
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(values, field.id)) {
|
|
63
|
+
visibleValues[field.id] = values[field.id];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return visibleValues;
|
|
67
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiTabAssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/MultiTabAssistantChat.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA4BxE,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"MultiTabAssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/MultiTabAssistantChat.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA4BxE,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;AA0B5B,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AA2a/B,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;IACzC,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAsGD,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpE;AAyED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,+BAA+B;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,kBAAkB,EAClB,OAAO,GAAG,UAAU,CACrB,GAAG;IACF,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5E,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC7E,sGAAsG;IACtG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;IACnD,2DAA2D;IAC3D,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,UAAiB,EACjB,YAAY,EACZ,aAAa,EACb,aAAqB,EACrB,MAAqD,EACrD,UAAU,EACV,mBAA0B,EAC1B,YAAY,EACZ,aAAqB,EACrB,KAAY,EACZ,GAAG,KAAK,EACT,EAAE,0BAA0B,qBAg1D5B"}
|
|
@@ -14,6 +14,7 @@ import { isTrustedFrameMessage } from "./frame.js";
|
|
|
14
14
|
import { RunStuckBanner } from "./RunStuckBanner.js";
|
|
15
15
|
import { callAction } from "./use-action.js";
|
|
16
16
|
import { useChangeVersion } from "./use-change-version.js";
|
|
17
|
+
import { CHAT_MODEL_SELECTION_CHANGED_EVENT } from "./use-chat-models.js";
|
|
17
18
|
import { useChatThreads, } from "./use-chat-threads.js";
|
|
18
19
|
import { cn } from "./utils.js";
|
|
19
20
|
/** The single path that hands a queued send to a mounted chat ref. */
|
|
@@ -66,6 +67,11 @@ function writeStoredModelSelection(key, selection) {
|
|
|
66
67
|
return;
|
|
67
68
|
try {
|
|
68
69
|
window.localStorage.setItem(key, JSON.stringify(selection));
|
|
70
|
+
queueMicrotask(() => {
|
|
71
|
+
window.dispatchEvent(new CustomEvent(CHAT_MODEL_SELECTION_CHANGED_EVENT, {
|
|
72
|
+
detail: { key },
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
69
75
|
}
|
|
70
76
|
catch { }
|
|
71
77
|
}
|
|
@@ -520,6 +526,35 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
520
526
|
const bumpModelSelectionVersion = useCallback(() => {
|
|
521
527
|
setModelSelectionVersion((version) => version + 1);
|
|
522
528
|
}, []);
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (typeof window === "undefined")
|
|
531
|
+
return;
|
|
532
|
+
const syncPersistedSelection = (event) => {
|
|
533
|
+
const detail = event
|
|
534
|
+
?.detail;
|
|
535
|
+
if (detail?.key && detail.key !== modelSelectionKey)
|
|
536
|
+
return;
|
|
537
|
+
const next = readStoredModelSelection(modelSelectionKey);
|
|
538
|
+
if (!next)
|
|
539
|
+
return;
|
|
540
|
+
const activeThreadId = activeThreadIdRef.current;
|
|
541
|
+
if (activeThreadId) {
|
|
542
|
+
threadModelRef.current.set(activeThreadId, next);
|
|
543
|
+
}
|
|
544
|
+
setPersistedModelSelection(next);
|
|
545
|
+
bumpModelSelectionVersion();
|
|
546
|
+
};
|
|
547
|
+
const handleStorage = (event) => {
|
|
548
|
+
if (event.key === modelSelectionKey)
|
|
549
|
+
syncPersistedSelection();
|
|
550
|
+
};
|
|
551
|
+
window.addEventListener(CHAT_MODEL_SELECTION_CHANGED_EVENT, syncPersistedSelection);
|
|
552
|
+
window.addEventListener("storage", handleStorage);
|
|
553
|
+
return () => {
|
|
554
|
+
window.removeEventListener(CHAT_MODEL_SELECTION_CHANGED_EVENT, syncPersistedSelection);
|
|
555
|
+
window.removeEventListener("storage", handleStorage);
|
|
556
|
+
};
|
|
557
|
+
}, [bumpModelSelectionVersion, modelSelectionKey]);
|
|
523
558
|
const postMessageSubmissionsDisabled = props.composerDisabled === true;
|
|
524
559
|
const setContextInTab = useCallback((threadId, item, options) => {
|
|
525
560
|
const ref = chatRefs.current.get(threadId);
|