dooers-agents-client 0.2.6 → 0.3.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/dist/main.cjs CHANGED
@@ -70,6 +70,82 @@ function toDisplayContentPart(w) {
70
70
  };
71
71
  }
72
72
  }
73
+ function toFormElement(w) {
74
+ const base = {
75
+ name: w.name,
76
+ label: w.label,
77
+ order: w.order ?? 0,
78
+ required: w.required ?? false,
79
+ disabled: w.disabled ?? false
80
+ };
81
+ switch (w.type) {
82
+ case "text_input":
83
+ return {
84
+ ...base,
85
+ type: "text_input",
86
+ placeholder: w.placeholder ?? null,
87
+ default: w.default ?? null,
88
+ inputType: w.input_type ?? "text"
89
+ };
90
+ case "textarea_input":
91
+ return {
92
+ ...base,
93
+ type: "textarea_input",
94
+ placeholder: w.placeholder ?? null,
95
+ default: w.default ?? null,
96
+ rows: w.rows ?? null
97
+ };
98
+ case "select_input":
99
+ return {
100
+ ...base,
101
+ type: "select_input",
102
+ options: w.options,
103
+ default: w.default ?? null,
104
+ placeholder: w.placeholder ?? null
105
+ };
106
+ case "radio_input":
107
+ return {
108
+ ...base,
109
+ type: "radio_input",
110
+ options: w.options,
111
+ default: w.default ?? null,
112
+ variant: w.variant ?? "native"
113
+ };
114
+ case "checkbox_input":
115
+ return {
116
+ ...base,
117
+ type: "checkbox_input",
118
+ options: w.options,
119
+ default: w.default ?? null,
120
+ variant: w.variant ?? "native"
121
+ };
122
+ case "file_input":
123
+ return {
124
+ ...base,
125
+ type: "file_input",
126
+ uploadUrl: w.upload_url,
127
+ accept: w.accept ?? null,
128
+ multiple: w.multiple ?? false
129
+ };
130
+ }
131
+ }
132
+ function toFormEventData(data) {
133
+ const elements = data.elements ?? [];
134
+ return {
135
+ message: data.message ?? "",
136
+ elements: elements.map(toFormElement),
137
+ submitLabel: data.submit_label ?? "Send",
138
+ cancelLabel: data.cancel_label ?? "Cancel",
139
+ size: data.size ?? "medium"
140
+ };
141
+ }
142
+ function toFormResponseEventData(data) {
143
+ return {
144
+ formEventId: data.form_event_id ?? "",
145
+ cancelled: data.cancelled ?? false,
146
+ values: data.values ?? {}
147
+ };
148
+ }
73
149
  function toThreadEvent(w) {
74
150
  return {
75
151
  id: w.id,
@@ -377,13 +453,7 @@ var WorkerClient = class {
377
453
  threadId: params.threadId ?? "",
378
454
  clientEventId
379
455
  });
380
- const promise = new Promise((resolve, reject) => {
381
- const timer = setTimeout(() => {
382
- this.pendingMessages.delete(clientEventId);
383
- reject(new Error("sendMessage timed out waiting for server response"));
384
- }, SEND_MESSAGE_TIMEOUT);
385
- this.pendingMessages.set(clientEventId, { resolve, timer });
386
- });
456
+ const promise = this.createPendingPromise(clientEventId);
387
457
  this.send("event.create", {
388
458
  thread_id: params.threadId,
389
459
  client_event_id: clientEventId,
@@ -395,7 +465,35 @@ var WorkerClient = class {
395
465
  });
396
466
  return promise;
397
467
  }
468
+ sendFormResponse(params) {
469
+ const clientEventId = crypto.randomUUID();
470
+ const promise = this.createPendingPromise(clientEventId);
471
+ this.send("event.create", {
472
+ thread_id: params.threadId,
473
+ client_event_id: clientEventId,
474
+ event: {
475
+ type: "form.response",
476
+ actor: "user",
477
+ content: [],
478
+ data: {
479
+ form_event_id: params.formEventId,
480
+ cancelled: params.cancelled,
481
+ values: params.values
482
+ }
483
+ }
484
+ });
485
+ return promise;
486
+ }
398
487
  // --- Private ---
488
+ createPendingPromise(clientEventId) {
489
+ return new Promise((resolve, reject) => {
490
+ const timer = setTimeout(() => {
491
+ this.pendingMessages.delete(clientEventId);
492
+ reject(new Error("Request timed out waiting for server response"));
493
+ }, SEND_MESSAGE_TIMEOUT);
494
+ this.pendingMessages.set(clientEventId, { resolve, timer });
495
+ });
496
+ }
399
497
  createConnection() {
400
498
  if (this.ws) {
401
499
  this.ws.onopen = null;
@@ -611,6 +709,23 @@ var WorkerClient = class {
611
709
  });
612
710
  }
613
711
  };
712
+ function extractFormStates(events, existing) {
713
+ let result = existing;
714
+ for (const e of events) {
715
+ if (e.type === "form.response" && e.data) {
716
+ const formEventId = e.data.form_event_id;
717
+ if (formEventId && !result[formEventId]) {
718
+ const cancelled = e.data.cancelled ?? false;
719
+ const values = e.data.values ?? {};
720
+ result = {
721
+ ...result,
722
+ [formEventId]: { values, submitting: false, submitted: !cancelled, cancelled }
723
+ };
724
+ }
725
+ }
726
+ }
727
+ return result;
728
+ }
614
729
  function createWorkerStore() {
615
730
  return vanilla.createStore()((set) => ({
616
731
  connection: {
@@ -635,6 +750,7 @@ function createWorkerStore() {
635
750
  isLoading: true
636
751
  },
637
752
  feedback: {},
753
+ formStates: {},
638
754
  analytics: {
639
755
  events: [],
640
756
  counters: { totalRequests: 0, feedbackLikes: 0, feedbackDislikes: 0 }
@@ -699,6 +815,14 @@ function createWorkerStore() {
699
815
  subscriptions.delete(threadId);
700
816
  const loadingThreads = new Set(s.loadingThreads);
701
817
  loadingThreads.delete(threadId);
818
+ const deletedEvents = s.events[threadId] ?? [];
819
+ const deletedEventIds = new Set(deletedEvents.map((e) => e.id));
820
+ const formStates = { ...s.formStates };
821
+ for (const key of Object.keys(formStates)) {
822
+ if (deletedEventIds.has(key)) {
823
+ delete formStates[key];
824
+ }
825
+ }
702
826
  return {
703
827
  threads,
704
828
  threadOrder,
@@ -708,7 +832,8 @@ function createWorkerStore() {
708
832
  optimisticKeys,
709
833
  eventPagination,
710
834
  subscriptions,
711
- loadingThreads
835
+ loadingThreads,
836
+ formStates
712
837
  };
713
838
  }),
714
839
  onThreadSnapshot: (thread, snapshotEvents, runs) => set((s) => {
@@ -736,7 +861,8 @@ function createWorkerStore() {
736
861
  runs: { ...s.runs, [thread.id]: runs },
737
862
  optimistic,
738
863
  optimisticKeys,
739
- loadingThreads
864
+ loadingThreads,
865
+ formStates: extractFormStates(mergedEvents, s.formStates)
740
866
  };
741
867
  }),
742
868
  onEventAppend: (threadId, newEvents) => set((s) => {
@@ -753,7 +879,8 @@ function createWorkerStore() {
753
879
  }
754
880
  const merged = updates.size > 0 ? existing.map((e) => updates.get(e.id) ?? e) : existing;
755
881
  return {
756
- events: { ...s.events, [threadId]: [...merged, ...appends] }
882
+ events: { ...s.events, [threadId]: [...merged, ...appends] },
883
+ formStates: extractFormStates(appends, s.formStates)
757
884
  };
758
885
  }),
759
886
  reconcileEvents: (threadId, newEvents, resolvedClientEventIds) => set((s) => {
@@ -875,7 +1002,64 @@ function createWorkerStore() {
875
1002
  events: [],
876
1003
  counters: { totalRequests: 0, feedbackLikes: 0, feedbackDislikes: 0 }
877
1004
  }
878
- }))
1005
+ })),
1006
+ initFormState: (formEventId, defaults) => set((s) => {
1007
+ if (s.formStates[formEventId]) return s;
1008
+ return {
1009
+ formStates: {
1010
+ ...s.formStates,
1011
+ [formEventId]: {
1012
+ values: defaults,
1013
+ submitting: false,
1014
+ submitted: false,
1015
+ cancelled: false
1016
+ }
1017
+ }
1018
+ };
1019
+ }),
1020
+ setFormSubmitting: (formEventId, submitting) => set((s) => {
1021
+ const existing = s.formStates[formEventId];
1022
+ if (!existing) return s;
1023
+ return {
1024
+ formStates: {
1025
+ ...s.formStates,
1026
+ [formEventId]: { ...existing, submitting }
1027
+ }
1028
+ };
1029
+ }),
1030
+ setFormValue: (formEventId, fieldName, value) => set((s) => {
1031
+ const existing = s.formStates[formEventId];
1032
+ if (!existing || existing.submitted || existing.cancelled) return s;
1033
+ return {
1034
+ formStates: {
1035
+ ...s.formStates,
1036
+ [formEventId]: {
1037
+ ...existing,
1038
+ values: { ...existing.values, [fieldName]: value }
1039
+ }
1040
+ }
1041
+ };
1042
+ }),
1043
+ setFormSubmitted: (formEventId) => set((s) => {
1044
+ const existing = s.formStates[formEventId];
1045
+ if (!existing) return s;
1046
+ return {
1047
+ formStates: {
1048
+ ...s.formStates,
1049
+ [formEventId]: { ...existing, submitting: false, submitted: true }
1050
+ }
1051
+ };
1052
+ }),
1053
+ setFormCancelled: (formEventId) => set((s) => {
1054
+ const existing = s.formStates[formEventId];
1055
+ if (!existing) return s;
1056
+ return {
1057
+ formStates: {
1058
+ ...s.formStates,
1059
+ [formEventId]: { ...existing, submitting: false, cancelled: true }
1060
+ }
1061
+ };
1062
+ })
879
1063
  }
880
1064
  }));
881
1065
  }
@@ -1095,6 +1279,148 @@ function useFeedback(targetId, targetType = "event") {
1095
1279
  );
1096
1280
  return { feedback, like, dislike };
1097
1281
  }
1282
+ function useForm(formEventId, formData, threadId) {
1283
+ const { client, store } = useWorkerContext();
1284
+ const formState = useStore((s) => s.formStates[formEventId]);
1285
+ const [errors, setErrors] = react.useState({});
1286
+ react.useEffect(() => {
1287
+ const defaults = {};
1288
+ for (const el of formData.elements) {
1289
+ if ("default" in el && el.default != null) {
1290
+ defaults[el.name] = el.default;
1291
+ }
1292
+ }
1293
+ store.getState().actions.initFormState(formEventId, defaults);
1294
+ }, [formEventId, formData.elements, store]);
1295
+ const values = formState?.values ?? {};
1296
+ const isSubmitting = formState?.submitting ?? false;
1297
+ const isSubmitted = formState?.submitted ?? false;
1298
+ const isCancelled = formState?.cancelled ?? false;
1299
+ const isDisabled = isSubmitting || isSubmitted || isCancelled;
1300
+ const setValue = react.useCallback(
1301
+ (fieldName, value) => {
1302
+ store.getState().actions.setFormValue(formEventId, fieldName, value);
1303
+ setErrors((prev) => {
1304
+ if (!prev[fieldName]) return prev;
1305
+ const next = { ...prev };
1306
+ delete next[fieldName];
1307
+ return next;
1308
+ });
1309
+ },
1310
+ [formEventId, store]
1311
+ );
1312
+ const validate = react.useCallback(() => {
1313
+ const currentValues = store.getState().formStates[formEventId]?.values ?? {};
1314
+ const newErrors = {};
1315
+ for (const el of formData.elements) {
1316
+ if (!el.required) continue;
1317
+ const val = currentValues[el.name];
1318
+ if (isEmptyValue(el, val)) {
1319
+ newErrors[el.name] = "Required";
1320
+ }
1321
+ }
1322
+ setErrors(newErrors);
1323
+ return Object.keys(newErrors).length === 0;
1324
+ }, [formEventId, formData.elements, store]);
1325
+ const submit = react.useCallback(async () => {
1326
+ const state = store.getState();
1327
+ const fs = state.formStates[formEventId];
1328
+ if (!fs || fs.submitting || fs.submitted || fs.cancelled) return;
1329
+ if (!validate()) return;
1330
+ state.actions.setFormSubmitting(formEventId, true);
1331
+ const currentValues = state.formStates[formEventId]?.values ?? {};
1332
+ if (!threadId) {
1333
+ state.actions.setFormSubmitted(formEventId);
1334
+ return;
1335
+ }
1336
+ try {
1337
+ await client.sendFormResponse({
1338
+ threadId,
1339
+ formEventId,
1340
+ cancelled: false,
1341
+ values: currentValues
1342
+ });
1343
+ store.getState().actions.setFormSubmitted(formEventId);
1344
+ } catch {
1345
+ store.getState().actions.setFormSubmitting(formEventId, false);
1346
+ }
1347
+ }, [formEventId, threadId, validate, client, store]);
1348
+ const cancel = react.useCallback(async () => {
1349
+ const state = store.getState();
1350
+ const fs = state.formStates[formEventId];
1351
+ if (!fs || fs.submitting || fs.submitted || fs.cancelled) return;
1352
+ state.actions.setFormSubmitting(formEventId, true);
1353
+ if (!threadId) {
1354
+ state.actions.setFormCancelled(formEventId);
1355
+ return;
1356
+ }
1357
+ try {
1358
+ await client.sendFormResponse({
1359
+ threadId,
1360
+ formEventId,
1361
+ cancelled: true,
1362
+ values: {}
1363
+ });
1364
+ store.getState().actions.setFormCancelled(formEventId);
1365
+ } catch {
1366
+ store.getState().actions.setFormSubmitting(formEventId, false);
1367
+ }
1368
+ }, [formEventId, threadId, client, store]);
1369
+ return {
1370
+ values,
1371
+ errors,
1372
+ setValue,
1373
+ validate,
1374
+ submit,
1375
+ cancel,
1376
+ isSubmitting,
1377
+ isSubmitted,
1378
+ isCancelled,
1379
+ isDisabled
1380
+ };
1381
+ }
1382
+ function isEmptyValue(element, val) {
1383
+ if (val == null) return true;
1384
+ if (typeof val === "string" && val.trim() === "") return true;
1385
+ if (element.type === "checkbox_input" && Array.isArray(val) && val.length === 0) return true;
1386
+ return false;
1387
+ }
1388
+ function useFormFileUpload() {
1389
+ const [isUploading, setIsUploading] = react.useState(false);
1390
+ const [error, setError] = react.useState(null);
1391
+ const upload = react.useCallback(
1392
+ async (params) => {
1393
+ setIsUploading(true);
1394
+ setError(null);
1395
+ try {
1396
+ const formData = new FormData();
1397
+ formData.append("file", params.file);
1398
+ formData.append("field_id", params.fieldId);
1399
+ formData.append("agent_id", params.agentId);
1400
+ formData.append("run_id", params.runId);
1401
+ formData.append("thread_id", params.threadId);
1402
+ const response = await fetch(params.uploadUrl, {
1403
+ method: "POST",
1404
+ body: formData
1405
+ });
1406
+ if (!response.ok) {
1407
+ const detail = await response.text().catch(() => "");
1408
+ throw new Error(detail || `Upload failed (${response.status})`);
1409
+ }
1410
+ return await response.json();
1411
+ } catch (err) {
1412
+ const message = err instanceof Error ? err.message : "Upload failed";
1413
+ setError(message);
1414
+ throw err;
1415
+ } finally {
1416
+ setIsUploading(false);
1417
+ }
1418
+ },
1419
+ []
1420
+ );
1421
+ const clearError = react.useCallback(() => setError(null), []);
1422
+ return { upload, isUploading, error, clearError };
1423
+ }
1098
1424
  function useMessage() {
1099
1425
  const { client } = useWorkerContext();
1100
1426
  const send = react.useCallback(
@@ -1148,7 +1474,7 @@ function useThreadDetails(threadId) {
1148
1474
  function useThreadEvents(threadId) {
1149
1475
  const { client } = useWorkerContext();
1150
1476
  const hasOlderEvents = useStore(
1151
- (s) => threadId ? s.eventPagination[threadId]?.hasMore ?? true : false
1477
+ (s) => threadId ? s.eventPagination[threadId]?.hasMore ?? false : false
1152
1478
  );
1153
1479
  const loadOlderEvents = react.useCallback(
1154
1480
  (limit) => {
@@ -1203,10 +1529,15 @@ function useUpload() {
1203
1529
 
1204
1530
  exports.WorkerProvider = WorkerProvider;
1205
1531
  exports.isSettingsFieldGroup = isSettingsFieldGroup;
1532
+ exports.toFormElement = toFormElement;
1533
+ exports.toFormEventData = toFormEventData;
1534
+ exports.toFormResponseEventData = toFormResponseEventData;
1206
1535
  exports.useAnalytics = useAnalytics;
1207
1536
  exports.useAudioRecorder = useAudioRecorder;
1208
1537
  exports.useConnection = useConnection;
1209
1538
  exports.useFeedback = useFeedback;
1539
+ exports.useForm = useForm;
1540
+ exports.useFormFileUpload = useFormFileUpload;
1210
1541
  exports.useMessage = useMessage;
1211
1542
  exports.useSettings = useSettings;
1212
1543
  exports.useThreadDetails = useThreadDetails;