dooers-agents-client 0.1.0 → 0.2.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 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": {
@@ -633,6 +703,27 @@ function createWorkerStore() {
633
703
  events: { ...s.events, [threadId]: [...existing, ...unique] }
634
704
  };
635
705
  }),
706
+ reconcileEvents: (threadId, newEvents, resolvedClientEventIds) => set((s) => {
707
+ const existing = s.events[threadId] ?? [];
708
+ const existingIds = new Set(existing.map((e) => e.id));
709
+ const unique = newEvents.filter((e) => !existingIds.has(e.id));
710
+ let optEvents = s.optimistic[threadId] ?? [];
711
+ let optKeys = s.optimisticKeys[threadId] ?? [];
712
+ for (const clientEventId of resolvedClientEventIds) {
713
+ const idx = optKeys.indexOf(clientEventId);
714
+ if (idx >= 0) {
715
+ optEvents = [...optEvents];
716
+ optEvents.splice(idx, 1);
717
+ optKeys = [...optKeys];
718
+ optKeys.splice(idx, 1);
719
+ }
720
+ }
721
+ return {
722
+ events: { ...s.events, [threadId]: [...existing, ...unique] },
723
+ optimistic: { ...s.optimistic, [threadId]: optEvents },
724
+ optimisticKeys: { ...s.optimisticKeys, [threadId]: optKeys }
725
+ };
726
+ }),
636
727
  onEventListResult: (threadId, olderEvents, cursor, hasMore) => set((s) => {
637
728
  const existing = s.events[threadId] ?? [];
638
729
  const existingIds = new Set(existing.map((e) => e.id));
@@ -748,6 +839,7 @@ function WorkerProvider({
748
839
  organizationRole,
749
840
  workspaceRole,
750
841
  authToken,
842
+ uploadUrl,
751
843
  onError,
752
844
  children
753
845
  }) {
@@ -760,6 +852,9 @@ function WorkerProvider({
760
852
  react.useEffect(() => {
761
853
  clientRef.current?.setOnError(onError ?? null);
762
854
  }, [onError]);
855
+ react.useEffect(() => {
856
+ clientRef.current?.setUploadUrl(uploadUrl);
857
+ }, [uploadUrl]);
763
858
  react.useEffect(() => {
764
859
  if (!url || !workerId) return;
765
860
  clientRef.current?.connect(url, workerId, {
@@ -818,6 +913,93 @@ function useAnalytics() {
818
913
  const unsubscribe = react.useCallback(() => client.unsubscribeAnalytics(), [client]);
819
914
  return { events, counters, subscribe, unsubscribe };
820
915
  }
916
+ function getPreferredMimeType() {
917
+ if (typeof MediaRecorder !== "undefined") {
918
+ if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) return "audio/webm;codecs=opus";
919
+ if (MediaRecorder.isTypeSupported("audio/webm")) return "audio/webm";
920
+ if (MediaRecorder.isTypeSupported("audio/mp4")) return "audio/mp4";
921
+ }
922
+ return "audio/webm";
923
+ }
924
+ function useAudioRecorder() {
925
+ const [isRecording, setIsRecording] = react.useState(false);
926
+ const [duration, setDuration] = react.useState(0);
927
+ const mediaRecorderRef = react.useRef(null);
928
+ const streamRef = react.useRef(null);
929
+ const chunksRef = react.useRef([]);
930
+ const timerRef = react.useRef(null);
931
+ const startTimeRef = react.useRef(0);
932
+ const resolveStopRef = react.useRef(null);
933
+ const releaseStream = react.useCallback(() => {
934
+ streamRef.current?.getTracks().forEach((t) => {
935
+ t.stop();
936
+ });
937
+ streamRef.current = null;
938
+ }, []);
939
+ const clearTimer = react.useCallback(() => {
940
+ if (timerRef.current) {
941
+ clearInterval(timerRef.current);
942
+ timerRef.current = null;
943
+ }
944
+ }, []);
945
+ react.useEffect(() => {
946
+ return () => {
947
+ if (mediaRecorderRef.current?.state === "recording") {
948
+ mediaRecorderRef.current.stop();
949
+ }
950
+ releaseStream();
951
+ clearTimer();
952
+ };
953
+ }, [releaseStream, clearTimer]);
954
+ const start = react.useCallback(async () => {
955
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
956
+ streamRef.current = stream;
957
+ const mimeType = getPreferredMimeType();
958
+ const recorder = new MediaRecorder(stream, { mimeType });
959
+ mediaRecorderRef.current = recorder;
960
+ chunksRef.current = [];
961
+ recorder.ondataavailable = (e) => {
962
+ if (e.data.size > 0) chunksRef.current.push(e.data);
963
+ };
964
+ recorder.onstop = () => {
965
+ const blob = new Blob(chunksRef.current, { type: mimeType.split(";")[0] });
966
+ releaseStream();
967
+ clearTimer();
968
+ resolveStopRef.current?.(blob);
969
+ resolveStopRef.current = null;
970
+ };
971
+ startTimeRef.current = Date.now();
972
+ setDuration(0);
973
+ timerRef.current = setInterval(() => {
974
+ setDuration(Math.floor((Date.now() - startTimeRef.current) / 1e3));
975
+ }, 1e3);
976
+ recorder.start();
977
+ setIsRecording(true);
978
+ }, [releaseStream, clearTimer]);
979
+ const stop = react.useCallback(() => {
980
+ return new Promise((resolve, reject) => {
981
+ if (!mediaRecorderRef.current || mediaRecorderRef.current.state === "inactive") {
982
+ reject(new Error("No active recording to stop"));
983
+ return;
984
+ }
985
+ resolveStopRef.current = resolve;
986
+ mediaRecorderRef.current.stop();
987
+ setIsRecording(false);
988
+ setDuration(0);
989
+ });
990
+ }, []);
991
+ const cancel = react.useCallback(() => {
992
+ resolveStopRef.current = null;
993
+ if (mediaRecorderRef.current?.state === "recording") {
994
+ mediaRecorderRef.current.stop();
995
+ }
996
+ releaseStream();
997
+ clearTimer();
998
+ setIsRecording(false);
999
+ setDuration(0);
1000
+ }, [releaseStream, clearTimer]);
1001
+ return { start, stop, cancel, isRecording, duration };
1002
+ }
821
1003
  function useConnection() {
822
1004
  const { client } = useWorkerContext();
823
1005
  const status = useStore((s) => s.connection.status);
@@ -884,7 +1066,7 @@ function useThreadDetails(threadId) {
884
1066
  const confirmed = useStore((s) => threadId ? s.events[threadId] ?? EMPTY_ARRAY : EMPTY_ARRAY);
885
1067
  const runs = useStore((s) => threadId ? s.runs[threadId] ?? EMPTY_RUNS : EMPTY_RUNS);
886
1068
  const events = react.useMemo(
887
- () => optimistic.length ? [...optimistic, ...confirmed] : confirmed,
1069
+ () => optimistic.length ? [...confirmed, ...optimistic] : confirmed,
888
1070
  [optimistic, confirmed]
889
1071
  );
890
1072
  return { thread, events, runs, isLoading, isWaiting };
@@ -923,10 +1105,32 @@ function useThreadsActions() {
923
1105
  const loadMore = react.useCallback((limit) => client.loadMoreThreads(limit), [client]);
924
1106
  return { deleteThread, loadMore };
925
1107
  }
1108
+ function useUpload() {
1109
+ const { client } = useWorkerContext();
1110
+ const [isUploading, setIsUploading] = react.useState(false);
1111
+ const activeCountRef = react.useRef(0);
1112
+ const upload = react.useCallback(
1113
+ async (file) => {
1114
+ activeCountRef.current++;
1115
+ setIsUploading(true);
1116
+ try {
1117
+ return await client.upload(file);
1118
+ } finally {
1119
+ activeCountRef.current--;
1120
+ if (activeCountRef.current === 0) {
1121
+ setIsUploading(false);
1122
+ }
1123
+ }
1124
+ },
1125
+ [client]
1126
+ );
1127
+ return { upload, isUploading };
1128
+ }
926
1129
 
927
1130
  exports.WorkerProvider = WorkerProvider;
928
1131
  exports.isSettingsFieldGroup = isSettingsFieldGroup;
929
1132
  exports.useAnalytics = useAnalytics;
1133
+ exports.useAudioRecorder = useAudioRecorder;
930
1134
  exports.useConnection = useConnection;
931
1135
  exports.useFeedback = useFeedback;
932
1136
  exports.useMessage = useMessage;
@@ -935,5 +1139,6 @@ exports.useThreadDetails = useThreadDetails;
935
1139
  exports.useThreadEvents = useThreadEvents;
936
1140
  exports.useThreadsActions = useThreadsActions;
937
1141
  exports.useThreadsList = useThreadsList;
1142
+ exports.useUpload = useUpload;
938
1143
  //# sourceMappingURL=main.cjs.map
939
1144
  //# sourceMappingURL=main.cjs.map