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 +6 -4
- package/dist/main.cjs +230 -16
- package/dist/main.cjs.map +1 -1
- package/dist/main.d.cts +65 -11
- package/dist/main.d.ts +65 -11
- package/dist/main.js +230 -18
- package/dist/main.js.map +1 -1
- package/package.json +4 -2
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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(
|
|
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",
|
|
147
|
+
return { type: "image", ref_id: p.refId };
|
|
124
148
|
case "document":
|
|
125
|
-
return { type: "document",
|
|
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:
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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,
|
|
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]:
|
|
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 ? [...
|
|
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
|