dooers-agents-client 0.1.0 → 0.2.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/README.md CHANGED
@@ -71,7 +71,9 @@ function Chat({ threadId }: { threadId: string }) {
71
71
  userId="user-1"
72
72
  userName="Alice"
73
73
  userEmail="alice@example.com"
74
- userRole="member"
74
+ systemRole="user" // "admin" | "user" (default: "user")
75
+ organizationRole="member" // "owner" | "manager" | "member" (default: "member")
76
+ workspaceRole="member" // "manager" | "member" (default: "member")
75
77
  authToken="sk-..." // Optional authentication
76
78
  onError={(err) => console.error(err.code, err.message)}
77
79
  >
@@ -212,11 +214,11 @@ All public types are camelCase. The SDK transforms the wire format (snake_case)
212
214
 
213
215
  ```typescript
214
216
  import type {
215
- Thread, // { id, workerId, title, createdAt, updatedAt, lastEventAt, metadata }
216
- ThreadEvent, // { id, threadId, type, actor, author, content, data, createdAt, ... }
217
+ User, // { userId, userName, userEmail, systemRole, organizationRole, workspaceRole }
218
+ Thread, // { id, workerId, organizationId, workspaceId, owner, users, title, ... }
219
+ ThreadEvent, // { id, threadId, type, actor, author, user, content, data, createdAt, ... }
217
220
  Run, // { id, threadId, agentId, status, startedAt, endedAt, error }
218
221
  ContentPart, // TextPart | ImagePart | DocumentPart
219
- Metadata, // { userId, userName, userEmail, userRole, organizationId, workspaceId }
220
222
  ConnectionStatus, // "idle" | "connecting" | "connected" | "disconnected" | "error"
221
223
  Actor, // "user" | "assistant" | "system" | "tool"
222
224
  EventType, // "message" | "tool.call" | "tool.result" | "tool.transaction" | ...
package/dist/main.cjs CHANGED
@@ -37,14 +37,36 @@ function toThread(w) {
37
37
  lastEventAt: w.last_event_at
38
38
  };
39
39
  }
40
- function toContentPart(w) {
40
+ function toDisplayContentPart(w) {
41
41
  switch (w.type) {
42
42
  case "text":
43
43
  return { type: "text", text: w.text };
44
+ case "audio":
45
+ return {
46
+ type: "audio",
47
+ url: w.url,
48
+ mimeType: w.mime_type,
49
+ duration: w.duration,
50
+ filename: w.filename
51
+ };
44
52
  case "image":
45
- return { type: "image", url: w.url, mimeType: w.mime_type, alt: w.alt };
53
+ return {
54
+ type: "image",
55
+ url: w.url,
56
+ mimeType: w.mime_type,
57
+ width: w.width,
58
+ height: w.height,
59
+ alt: w.alt,
60
+ filename: w.filename
61
+ };
46
62
  case "document":
47
- return { type: "document", url: w.url, filename: w.filename, mimeType: w.mime_type };
63
+ return {
64
+ type: "document",
65
+ url: w.url,
66
+ filename: w.filename,
67
+ mimeType: w.mime_type,
68
+ sizeBytes: w.size_bytes
69
+ };
48
70
  }
49
71
  }
50
72
  function toThreadEvent(w) {
@@ -56,7 +78,7 @@ function toThreadEvent(w) {
56
78
  actor: w.actor,
57
79
  author: w.author,
58
80
  user: toUser(w.user),
59
- content: w.content?.map(toContentPart),
81
+ content: w.content?.map(toDisplayContentPart),
60
82
  data: w.data,
61
83
  createdAt: w.created_at,
62
84
  clientEventId: w.client_event_id
@@ -119,10 +141,12 @@ function toWireContentPart(p) {
119
141
  switch (p.type) {
120
142
  case "text":
121
143
  return { type: "text", text: p.text };
144
+ case "audio":
145
+ return { type: "audio", ref_id: p.refId, duration: p.duration };
122
146
  case "image":
123
- return { type: "image", url: p.url, mime_type: p.mimeType, alt: p.alt };
147
+ return { type: "image", ref_id: p.refId };
124
148
  case "document":
125
- return { type: "document", url: p.url, filename: p.filename, mime_type: p.mimeType };
149
+ return { type: "document", ref_id: p.refId };
126
150
  }
127
151
  }
128
152
 
@@ -136,6 +160,7 @@ var WorkerClient = class {
136
160
  onError = null;
137
161
  url = "";
138
162
  workerId = "";
163
+ uploadUrl;
139
164
  config = {
140
165
  organizationId: "",
141
166
  workspaceId: "",
@@ -164,6 +189,35 @@ var WorkerClient = class {
164
189
  setOnError(cb) {
165
190
  this.onError = cb;
166
191
  }
192
+ setUploadUrl(url) {
193
+ this.uploadUrl = url;
194
+ }
195
+ async upload(file) {
196
+ if (!this.uploadUrl) {
197
+ throw new Error("uploadUrl not configured");
198
+ }
199
+ const formData = new FormData();
200
+ formData.append("file", file);
201
+ const headers = {};
202
+ if (this.config.authToken) {
203
+ headers.Authorization = `Bearer ${this.config.authToken}`;
204
+ }
205
+ const response = await fetch(this.uploadUrl, {
206
+ method: "POST",
207
+ body: formData,
208
+ headers
209
+ });
210
+ if (!response.ok) {
211
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
212
+ }
213
+ const result = await response.json();
214
+ return {
215
+ refId: result.ref_id,
216
+ mimeType: result.mime_type,
217
+ filename: result.filename,
218
+ sizeBytes: result.size_bytes
219
+ };
220
+ }
167
221
  connect(url, workerId, config) {
168
222
  this.url = url;
169
223
  this.workerId = workerId;
@@ -270,6 +324,20 @@ var WorkerClient = class {
270
324
  sendMessage(params) {
271
325
  const clientEventId = crypto.randomUUID();
272
326
  const content = params.content ? params.content.map(toWireContentPart) : [{ type: "text", text: params.text ?? "" }];
327
+ const displayContent = params.content ? params.content.map((p) => {
328
+ switch (p.type) {
329
+ case "text":
330
+ return { type: "text", text: p.text };
331
+ case "audio":
332
+ return { type: "audio", duration: p.duration };
333
+ case "image":
334
+ return { type: "image" };
335
+ case "document":
336
+ return { type: "document" };
337
+ default:
338
+ return { type: "text", text: "" };
339
+ }
340
+ }) : [{ type: "text", text: params.text ?? "" }];
273
341
  const optimisticEvent = {
274
342
  id: `optimistic-${clientEventId}`,
275
343
  threadId: params.threadId ?? "",
@@ -285,7 +353,7 @@ var WorkerClient = class {
285
353
  organizationRole: this.config.organizationRole ?? "member",
286
354
  workspaceRole: this.config.workspaceRole ?? "member"
287
355
  },
288
- content: params.content ?? [{ type: "text", text: params.text ?? "" }],
356
+ content: displayContent,
289
357
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
290
358
  };
291
359
  if (params.threadId) {
@@ -420,13 +488,11 @@ var WorkerClient = class {
420
488
  }
421
489
  case "event.append": {
422
490
  const events = frame.payload.events.map(toThreadEvent);
491
+ const resolvedClientEventIds = [];
423
492
  for (const event of events) {
424
493
  if (event.clientEventId && this.pendingOptimistic.has(event.clientEventId)) {
425
- const pending = this.pendingOptimistic.get(event.clientEventId);
426
- if (pending) {
427
- this.callbacks.removeOptimistic(pending.threadId, event.clientEventId);
428
- this.pendingOptimistic.delete(event.clientEventId);
429
- }
494
+ resolvedClientEventIds.push(event.clientEventId);
495
+ this.pendingOptimistic.delete(event.clientEventId);
430
496
  }
431
497
  if (event.clientEventId && this.pendingMessages.has(event.clientEventId)) {
432
498
  const pendingMessage = this.pendingMessages.get(event.clientEventId);
@@ -441,7 +507,11 @@ var WorkerClient = class {
441
507
  if (lastEvent) {
442
508
  this.lastEventIds.set(frame.payload.thread_id, lastEvent.id);
443
509
  }
444
- this.callbacks.onEventAppend(frame.payload.thread_id, events);
510
+ if (resolvedClientEventIds.length > 0) {
511
+ this.callbacks.reconcileEvents(frame.payload.thread_id, events, resolvedClientEventIds);
512
+ } else {
513
+ this.callbacks.onEventAppend(frame.payload.thread_id, events);
514
+ }
445
515
  break;
446
516
  }
447
517
  case "event.list.result": {
@@ -612,15 +682,24 @@ function createWorkerStore() {
612
682
  loadingThreads
613
683
  };
614
684
  }),
615
- onThreadSnapshot: (thread, events, runs) => set((s) => {
685
+ onThreadSnapshot: (thread, snapshotEvents, runs) => set((s) => {
616
686
  const threads = { ...s.threads, [thread.id]: thread };
617
687
  const threadOrder = s.threadOrder.includes(thread.id) ? s.threadOrder : [thread.id, ...s.threadOrder];
618
688
  const loadingThreads = new Set(s.loadingThreads);
619
689
  loadingThreads.delete(thread.id);
690
+ const existing = s.events[thread.id] ?? [];
691
+ let mergedEvents;
692
+ if (existing.length === 0) {
693
+ mergedEvents = snapshotEvents;
694
+ } else {
695
+ const snapshotIds = new Set(snapshotEvents.map((e) => e.id));
696
+ const extra = existing.filter((e) => !snapshotIds.has(e.id));
697
+ mergedEvents = extra.length > 0 ? [...snapshotEvents, ...extra] : snapshotEvents;
698
+ }
620
699
  return {
621
700
  threads,
622
701
  threadOrder,
623
- events: { ...s.events, [thread.id]: events },
702
+ events: { ...s.events, [thread.id]: mergedEvents },
624
703
  runs: { ...s.runs, [thread.id]: runs },
625
704
  loadingThreads
626
705
  };
@@ -633,6 +712,27 @@ function createWorkerStore() {
633
712
  events: { ...s.events, [threadId]: [...existing, ...unique] }
634
713
  };
635
714
  }),
715
+ reconcileEvents: (threadId, newEvents, resolvedClientEventIds) => set((s) => {
716
+ const existing = s.events[threadId] ?? [];
717
+ const existingIds = new Set(existing.map((e) => e.id));
718
+ const unique = newEvents.filter((e) => !existingIds.has(e.id));
719
+ let optEvents = s.optimistic[threadId] ?? [];
720
+ let optKeys = s.optimisticKeys[threadId] ?? [];
721
+ for (const clientEventId of resolvedClientEventIds) {
722
+ const idx = optKeys.indexOf(clientEventId);
723
+ if (idx >= 0) {
724
+ optEvents = [...optEvents];
725
+ optEvents.splice(idx, 1);
726
+ optKeys = [...optKeys];
727
+ optKeys.splice(idx, 1);
728
+ }
729
+ }
730
+ return {
731
+ events: { ...s.events, [threadId]: [...existing, ...unique] },
732
+ optimistic: { ...s.optimistic, [threadId]: optEvents },
733
+ optimisticKeys: { ...s.optimisticKeys, [threadId]: optKeys }
734
+ };
735
+ }),
636
736
  onEventListResult: (threadId, olderEvents, cursor, hasMore) => set((s) => {
637
737
  const existing = s.events[threadId] ?? [];
638
738
  const existingIds = new Set(existing.map((e) => e.id));
@@ -748,6 +848,7 @@ function WorkerProvider({
748
848
  organizationRole,
749
849
  workspaceRole,
750
850
  authToken,
851
+ uploadUrl,
751
852
  onError,
752
853
  children
753
854
  }) {
@@ -760,6 +861,9 @@ function WorkerProvider({
760
861
  react.useEffect(() => {
761
862
  clientRef.current?.setOnError(onError ?? null);
762
863
  }, [onError]);
864
+ react.useEffect(() => {
865
+ clientRef.current?.setUploadUrl(uploadUrl);
866
+ }, [uploadUrl]);
763
867
  react.useEffect(() => {
764
868
  if (!url || !workerId) return;
765
869
  clientRef.current?.connect(url, workerId, {
@@ -818,6 +922,93 @@ function useAnalytics() {
818
922
  const unsubscribe = react.useCallback(() => client.unsubscribeAnalytics(), [client]);
819
923
  return { events, counters, subscribe, unsubscribe };
820
924
  }
925
+ function getPreferredMimeType() {
926
+ if (typeof MediaRecorder !== "undefined") {
927
+ if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) return "audio/webm;codecs=opus";
928
+ if (MediaRecorder.isTypeSupported("audio/webm")) return "audio/webm";
929
+ if (MediaRecorder.isTypeSupported("audio/mp4")) return "audio/mp4";
930
+ }
931
+ return "audio/webm";
932
+ }
933
+ function useAudioRecorder() {
934
+ const [isRecording, setIsRecording] = react.useState(false);
935
+ const [duration, setDuration] = react.useState(0);
936
+ const mediaRecorderRef = react.useRef(null);
937
+ const streamRef = react.useRef(null);
938
+ const chunksRef = react.useRef([]);
939
+ const timerRef = react.useRef(null);
940
+ const startTimeRef = react.useRef(0);
941
+ const resolveStopRef = react.useRef(null);
942
+ const releaseStream = react.useCallback(() => {
943
+ streamRef.current?.getTracks().forEach((t) => {
944
+ t.stop();
945
+ });
946
+ streamRef.current = null;
947
+ }, []);
948
+ const clearTimer = react.useCallback(() => {
949
+ if (timerRef.current) {
950
+ clearInterval(timerRef.current);
951
+ timerRef.current = null;
952
+ }
953
+ }, []);
954
+ react.useEffect(() => {
955
+ return () => {
956
+ if (mediaRecorderRef.current?.state === "recording") {
957
+ mediaRecorderRef.current.stop();
958
+ }
959
+ releaseStream();
960
+ clearTimer();
961
+ };
962
+ }, [releaseStream, clearTimer]);
963
+ const start = react.useCallback(async () => {
964
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
965
+ streamRef.current = stream;
966
+ const mimeType = getPreferredMimeType();
967
+ const recorder = new MediaRecorder(stream, { mimeType });
968
+ mediaRecorderRef.current = recorder;
969
+ chunksRef.current = [];
970
+ recorder.ondataavailable = (e) => {
971
+ if (e.data.size > 0) chunksRef.current.push(e.data);
972
+ };
973
+ recorder.onstop = () => {
974
+ const blob = new Blob(chunksRef.current, { type: mimeType.split(";")[0] });
975
+ releaseStream();
976
+ clearTimer();
977
+ resolveStopRef.current?.(blob);
978
+ resolveStopRef.current = null;
979
+ };
980
+ startTimeRef.current = Date.now();
981
+ setDuration(0);
982
+ timerRef.current = setInterval(() => {
983
+ setDuration(Math.floor((Date.now() - startTimeRef.current) / 1e3));
984
+ }, 1e3);
985
+ recorder.start();
986
+ setIsRecording(true);
987
+ }, [releaseStream, clearTimer]);
988
+ const stop = react.useCallback(() => {
989
+ return new Promise((resolve, reject) => {
990
+ if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
991
+ reject(new Error("No active recording to stop"));
992
+ return;
993
+ }
994
+ resolveStopRef.current = resolve;
995
+ mediaRecorderRef.current.stop();
996
+ setIsRecording(false);
997
+ setDuration(0);
998
+ });
999
+ }, []);
1000
+ const cancel = react.useCallback(() => {
1001
+ resolveStopRef.current = null;
1002
+ if (mediaRecorderRef.current?.state === "recording") {
1003
+ mediaRecorderRef.current.stop();
1004
+ }
1005
+ releaseStream();
1006
+ clearTimer();
1007
+ setIsRecording(false);
1008
+ setDuration(0);
1009
+ }, [releaseStream, clearTimer]);
1010
+ return { start, stop, cancel, isRecording, duration };
1011
+ }
821
1012
  function useConnection() {
822
1013
  const { client } = useWorkerContext();
823
1014
  const status = useStore((s) => s.connection.status);
@@ -884,7 +1075,7 @@ function useThreadDetails(threadId) {
884
1075
  const confirmed = useStore((s) => threadId ? s.events[threadId] ?? EMPTY_ARRAY : EMPTY_ARRAY);
885
1076
  const runs = useStore((s) => threadId ? s.runs[threadId] ?? EMPTY_RUNS : EMPTY_RUNS);
886
1077
  const events = react.useMemo(
887
- () => optimistic.length ? [...optimistic, ...confirmed] : confirmed,
1078
+ () => optimistic.length ? [...confirmed, ...optimistic] : confirmed,
888
1079
  [optimistic, confirmed]
889
1080
  );
890
1081
  return { thread, events, runs, isLoading, isWaiting };
@@ -923,10 +1114,32 @@ function useThreadsActions() {
923
1114
  const loadMore = react.useCallback((limit) => client.loadMoreThreads(limit), [client]);
924
1115
  return { deleteThread, loadMore };
925
1116
  }
1117
+ function useUpload() {
1118
+ const { client } = useWorkerContext();
1119
+ const [isUploading, setIsUploading] = react.useState(false);
1120
+ const activeCountRef = react.useRef(0);
1121
+ const upload = react.useCallback(
1122
+ async (file) => {
1123
+ activeCountRef.current++;
1124
+ setIsUploading(true);
1125
+ try {
1126
+ return await client.upload(file);
1127
+ } finally {
1128
+ activeCountRef.current--;
1129
+ if (activeCountRef.current === 0) {
1130
+ setIsUploading(false);
1131
+ }
1132
+ }
1133
+ },
1134
+ [client]
1135
+ );
1136
+ return { upload, isUploading };
1137
+ }
926
1138
 
927
1139
  exports.WorkerProvider = WorkerProvider;
928
1140
  exports.isSettingsFieldGroup = isSettingsFieldGroup;
929
1141
  exports.useAnalytics = useAnalytics;
1142
+ exports.useAudioRecorder = useAudioRecorder;
930
1143
  exports.useConnection = useConnection;
931
1144
  exports.useFeedback = useFeedback;
932
1145
  exports.useMessage = useMessage;
@@ -935,5 +1148,6 @@ exports.useThreadDetails = useThreadDetails;
935
1148
  exports.useThreadEvents = useThreadEvents;
936
1149
  exports.useThreadsActions = useThreadsActions;
937
1150
  exports.useThreadsList = useThreadsList;
1151
+ exports.useUpload = useUpload;
938
1152
  //# sourceMappingURL=main.cjs.map
939
1153
  //# sourceMappingURL=main.cjs.map