@blocksdiy/react-common 1.70.2 → 1.71.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/comments/anchor.d.ts +74 -0
- package/dist/comments/anchor.d.ts.map +1 -0
- package/dist/comments/anchor.js +279 -0
- package/dist/comments/api.d.ts +38 -0
- package/dist/comments/api.d.ts.map +1 -0
- package/dist/comments/api.js +62 -0
- package/dist/comments/comment-agents.d.ts +28 -0
- package/dist/comments/comment-agents.d.ts.map +1 -0
- package/dist/comments/comment-agents.js +32 -0
- package/dist/comments/comment-mode.d.ts +97 -0
- package/dist/comments/comment-mode.d.ts.map +1 -0
- package/dist/comments/comment-mode.js +159 -0
- package/dist/comments/composer.d.ts +133 -0
- package/dist/comments/composer.d.ts.map +1 -0
- package/dist/comments/composer.js +273 -0
- package/dist/comments/index.d.ts +13 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +17 -0
- package/dist/comments/layout.d.ts +25 -0
- package/dist/comments/layout.d.ts.map +1 -0
- package/dist/comments/layout.js +83 -0
- package/dist/comments/mentions.d.ts +32 -0
- package/dist/comments/mentions.d.ts.map +1 -0
- package/dist/comments/mentions.js +30 -0
- package/dist/comments/page-pick.d.ts +76 -0
- package/dist/comments/page-pick.d.ts.map +1 -0
- package/dist/comments/page-pick.js +267 -0
- package/dist/comments/pick-message.d.ts +11 -0
- package/dist/comments/pick-message.d.ts.map +1 -0
- package/dist/comments/pick-message.js +33 -0
- package/dist/comments/pins.d.ts +72 -0
- package/dist/comments/pins.d.ts.map +1 -0
- package/dist/comments/pins.js +125 -0
- package/dist/comments/text-pick.d.ts +59 -0
- package/dist/comments/text-pick.d.ts.map +1 -0
- package/dist/comments/text-pick.js +165 -0
- package/dist/comments/threads-store.d.ts +119 -0
- package/dist/comments/threads-store.d.ts.map +1 -0
- package/dist/comments/threads-store.js +286 -0
- package/dist/comments/types.d.ts +232 -0
- package/dist/comments/types.d.ts.map +1 -0
- package/dist/comments/types.js +109 -0
- package/dist/hooks/agent-conversation-context.d.ts +12 -1
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
- package/dist/hooks/agent-conversation-context.js +12 -5
- package/dist/hooks/agent-thread-live-update.d.ts +5 -2
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +14 -0
- package/dist/hooks/use-agent-conversation.d.ts +8 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +10 -0
- package/dist/hooks/use-agent-threads.d.ts +1 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +4 -1
- package/dist/iframe-communication/types.d.ts +24 -0
- package/dist/iframe-communication/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
|
|
2
|
+
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import { CommentsRequestError } from "./api.js";
|
|
4
|
+
import { CommentRealtimeEventTypes, } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The comment threads of one page of an app, kept current: the page's threads, the open thread's
|
|
7
|
+
* comments, and the people who can be mentioned. Every change another tab makes, and every step of
|
|
8
|
+
* an agent's reply (this tab's mention included), arrives as an ids-only `COMMENTS_CHANGED` ping,
|
|
9
|
+
* and the store refetches that thread by id, which applies who may see it (a 404 removes it).
|
|
10
|
+
* Writes return the thread as the server saved it; either way a thread is replaced by id, and an
|
|
11
|
+
* answer that crossed a newer one is read again rather than set the thread back.
|
|
12
|
+
*/
|
|
13
|
+
/** A page with more threads than this shows the newest ones. */
|
|
14
|
+
export const MAX_THREADS_PER_PAGE = 500;
|
|
15
|
+
/** Pings arriving together are refetched together. */
|
|
16
|
+
export const LIVE_BATCH_MS = 150;
|
|
17
|
+
const EMPTY = {
|
|
18
|
+
status: "idle",
|
|
19
|
+
threads: [],
|
|
20
|
+
details: new Map(),
|
|
21
|
+
threadLoads: new Map(),
|
|
22
|
+
mentionableUsers: [],
|
|
23
|
+
};
|
|
24
|
+
/** A thread's summary: everything but what only an opened thread carries. */
|
|
25
|
+
const summaryOf = ({ resolvedByUserId: _resolvedByUserId, resolvedByAgentId: _resolvedByAgentId, resolvedAt: _resolvedAt, comments: _comments, ...summary }) => summary;
|
|
26
|
+
const byNewest = (a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt) || b.id - a.id;
|
|
27
|
+
export class CommentThreadsStore {
|
|
28
|
+
options;
|
|
29
|
+
snapshot = EMPTY;
|
|
30
|
+
listeners = new Set();
|
|
31
|
+
routePath = null;
|
|
32
|
+
/** Bumps on every route change: an answer for an older route is dropped. */
|
|
33
|
+
generation = 0;
|
|
34
|
+
stopPings = null;
|
|
35
|
+
pendingIds = new Set();
|
|
36
|
+
batchTimer = null;
|
|
37
|
+
/** Answers taken in so far; `takenAt` holds the count as of each thread's latest one. */
|
|
38
|
+
taken = 0;
|
|
39
|
+
takenAt = new Map();
|
|
40
|
+
setTimer;
|
|
41
|
+
clearTimer;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
45
|
+
this.clearTimer = options.clearTimer ?? ((id) => clearTimeout(id));
|
|
46
|
+
}
|
|
47
|
+
getSnapshot = () => this.snapshot;
|
|
48
|
+
subscribe = (listener) => {
|
|
49
|
+
this.listeners.add(listener);
|
|
50
|
+
return () => {
|
|
51
|
+
this.listeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** Shows the threads of `routePath` (null: none, and stop listening). */
|
|
55
|
+
async setRoute(routePath) {
|
|
56
|
+
if (routePath === this.routePath) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.routePath = routePath;
|
|
60
|
+
const generation = ++this.generation;
|
|
61
|
+
this.cancelBatch();
|
|
62
|
+
if (routePath === null) {
|
|
63
|
+
this.stopPings?.();
|
|
64
|
+
this.stopPings = null;
|
|
65
|
+
this.set(EMPTY);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.stopPings ??= this.options.subscribe((payload) => this.onPing(payload));
|
|
69
|
+
this.set({ ...EMPTY, status: "loading", mentionableUsers: this.snapshot.mentionableUsers });
|
|
70
|
+
try {
|
|
71
|
+
const [threads, users] = await Promise.all([
|
|
72
|
+
this.loadAll(routePath),
|
|
73
|
+
this.snapshot.mentionableUsers.length
|
|
74
|
+
? Promise.resolve(this.snapshot.mentionableUsers)
|
|
75
|
+
: this.options.api.mentionableUsers().then(({ users }) => users),
|
|
76
|
+
]);
|
|
77
|
+
if (generation === this.generation) {
|
|
78
|
+
// Threads that arrived while the list loaded (a ping, this tab's own write) are newer than
|
|
79
|
+
// the list's copy of them: they win, and their loaded comments stay.
|
|
80
|
+
const arrived = this.snapshot.threads;
|
|
81
|
+
const arrivedIds = new Set(arrived.map((thread) => thread.id));
|
|
82
|
+
this.set({
|
|
83
|
+
status: "ready",
|
|
84
|
+
// The list comes newest first, and what arrived meanwhile is newer still.
|
|
85
|
+
threads: [...arrived, ...threads.filter((thread) => !arrivedIds.has(thread.id))],
|
|
86
|
+
details: this.snapshot.details,
|
|
87
|
+
threadLoads: this.snapshot.threadLoads,
|
|
88
|
+
mentionableUsers: users,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
if (generation === this.generation) {
|
|
94
|
+
this.set({ ...this.snapshot, status: "error" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Loads a thread's comments (when it is opened). Until they are in, `threadLoads` says where it
|
|
100
|
+
* stands; a thread already loaded stays shown while it is read again.
|
|
101
|
+
*/
|
|
102
|
+
async loadThread(threadId) {
|
|
103
|
+
const generation = this.generation;
|
|
104
|
+
this.setLoad(threadId, "loading");
|
|
105
|
+
const thread = await this.refetch(threadId);
|
|
106
|
+
if (!thread && generation === this.generation && this.snapshot.threadLoads.get(threadId) === "loading") {
|
|
107
|
+
this.setLoad(threadId, "failed");
|
|
108
|
+
}
|
|
109
|
+
return thread;
|
|
110
|
+
}
|
|
111
|
+
createThread(anchor, body) {
|
|
112
|
+
return this.write(() => this.options.api.createThread({ anchor, body }));
|
|
113
|
+
}
|
|
114
|
+
addComment(threadId, body) {
|
|
115
|
+
return this.write(() => this.options.api.addComment(threadId, body));
|
|
116
|
+
}
|
|
117
|
+
updateComment(commentId, body) {
|
|
118
|
+
return this.write(() => this.options.api.updateComment(commentId, body));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Deletes one of the reader's comments. Null when it was the thread's last (no person's or agent's
|
|
122
|
+
* left): the thread is gone, and goes from the store as a 404 takes it (`not_found`).
|
|
123
|
+
*/
|
|
124
|
+
async deleteComment(commentId) {
|
|
125
|
+
// The thread it is in, known before the answer: an answer with no thread doesn't name it.
|
|
126
|
+
const threadId = [...this.snapshot.details.values()].find((thread) => thread.comments.some((comment) => comment.id === commentId))?.id;
|
|
127
|
+
const generation = this.generation;
|
|
128
|
+
const thread = await this.write(() => this.options.api.deleteComment(commentId));
|
|
129
|
+
if (!thread && threadId !== undefined && generation === this.generation) {
|
|
130
|
+
this.remove(threadId);
|
|
131
|
+
this.setLoad(threadId, "not_found");
|
|
132
|
+
}
|
|
133
|
+
return thread;
|
|
134
|
+
}
|
|
135
|
+
setResolved(threadId, resolved) {
|
|
136
|
+
const api = this.options.api;
|
|
137
|
+
return this.write(() => (resolved ? api.resolveThread(threadId) : api.reopenThread(threadId)));
|
|
138
|
+
}
|
|
139
|
+
dispose() {
|
|
140
|
+
this.cancelBatch();
|
|
141
|
+
this.stopPings?.();
|
|
142
|
+
this.stopPings = null;
|
|
143
|
+
// A remount (React StrictMode in dev) sets the route again: it must load and listen anew.
|
|
144
|
+
this.routePath = null;
|
|
145
|
+
this.generation += 1;
|
|
146
|
+
}
|
|
147
|
+
async loadAll(routePath) {
|
|
148
|
+
const threads = [];
|
|
149
|
+
let cursor;
|
|
150
|
+
do {
|
|
151
|
+
const page = await this.options.api.listThreads(routePath, { cursor });
|
|
152
|
+
threads.push(...page.threads);
|
|
153
|
+
cursor = page.nextCursor ?? undefined;
|
|
154
|
+
} while (cursor && threads.length < MAX_THREADS_PER_PAGE);
|
|
155
|
+
return threads.slice(0, MAX_THREADS_PER_PAGE);
|
|
156
|
+
}
|
|
157
|
+
onPing(payload) {
|
|
158
|
+
if (payload.appId !== this.options.appId || this.routePath === null) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.pendingIds.add(payload.threadId);
|
|
162
|
+
this.batchTimer ??= this.setTimer(() => {
|
|
163
|
+
this.batchTimer = null;
|
|
164
|
+
const ids = [...this.pendingIds];
|
|
165
|
+
this.pendingIds.clear();
|
|
166
|
+
ids.forEach((id) => void this.refetch(id));
|
|
167
|
+
}, LIVE_BATCH_MS);
|
|
168
|
+
}
|
|
169
|
+
cancelBatch() {
|
|
170
|
+
if (this.batchTimer !== null) {
|
|
171
|
+
this.clearTimer(this.batchTimer);
|
|
172
|
+
this.batchTimer = null;
|
|
173
|
+
}
|
|
174
|
+
this.pendingIds.clear();
|
|
175
|
+
}
|
|
176
|
+
/*
|
|
177
|
+
* One rule for every answer, a read's or a write's: when the thread took another answer after this
|
|
178
|
+
* request was sent, this one may be the older of the two, so it is not taken; the thread is read
|
|
179
|
+
* again instead (a read sent now holds both) and that answer is taken. Once: steady pings can't
|
|
180
|
+
* make it loop.
|
|
181
|
+
*/
|
|
182
|
+
/** Whether the thread took an answer after a request sent at `sentAt` (a count of answers taken). */
|
|
183
|
+
isCrossed(threadId, sentAt) {
|
|
184
|
+
return (this.takenAt.get(threadId) ?? 0) > sentAt;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Reads a thread and takes in the answer, by the rule above (`mayRetry` false: this is the read
|
|
188
|
+
* again, taken as it is). A 404 removes the thread. Returns the thread as the store has it now.
|
|
189
|
+
*/
|
|
190
|
+
async refetch(threadId, mayRetry = true) {
|
|
191
|
+
const generation = this.generation;
|
|
192
|
+
const sentAt = this.taken;
|
|
193
|
+
let thread;
|
|
194
|
+
try {
|
|
195
|
+
thread = await this.options.api.getThread(threadId);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error instanceof CommentsRequestError && error.status === 404 && generation === this.generation) {
|
|
199
|
+
this.remove(threadId);
|
|
200
|
+
this.setLoad(threadId, "not_found");
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
if (generation !== this.generation) {
|
|
205
|
+
return thread;
|
|
206
|
+
}
|
|
207
|
+
if (mayRetry && this.isCrossed(threadId, sentAt)) {
|
|
208
|
+
// Should the read again fail, the newer answer the thread took stays.
|
|
209
|
+
return (await this.refetch(threadId, false)) ?? this.snapshot.details.get(threadId) ?? null;
|
|
210
|
+
}
|
|
211
|
+
return this.apply(thread);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Sends a write and takes in its answer, by the rule above. Should reading the thread again fail,
|
|
215
|
+
* the write's answer is taken all the same, unless the thread is gone: the person's own change
|
|
216
|
+
* must show, and their tab gets no ping for it.
|
|
217
|
+
*/
|
|
218
|
+
async write(send) {
|
|
219
|
+
const sentAt = this.taken;
|
|
220
|
+
const thread = await send();
|
|
221
|
+
// No thread in the answer: the caller says what became of it.
|
|
222
|
+
if (!thread) {
|
|
223
|
+
return thread;
|
|
224
|
+
}
|
|
225
|
+
if (this.isCrossed(thread.id, sentAt)) {
|
|
226
|
+
const reread = await this.refetch(thread.id, false);
|
|
227
|
+
if (reread || !this.snapshot.details.has(thread.id)) {
|
|
228
|
+
return (reread ?? thread);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return this.apply(thread);
|
|
232
|
+
}
|
|
233
|
+
/** Takes in a thread as the server returned it. A thread of another page is kept out of the list. */
|
|
234
|
+
apply(thread) {
|
|
235
|
+
this.takenAt.set(thread.id, ++this.taken);
|
|
236
|
+
const details = new Map(this.snapshot.details);
|
|
237
|
+
details.set(thread.id, thread);
|
|
238
|
+
const others = this.snapshot.threads.filter((existing) => existing.id !== thread.id);
|
|
239
|
+
const threads = thread.routePath === this.routePath ? [summaryOf(thread), ...others].sort(byNewest) : others;
|
|
240
|
+
const threadLoads = new Map(this.snapshot.threadLoads);
|
|
241
|
+
threadLoads.delete(thread.id);
|
|
242
|
+
this.set({ ...this.snapshot, threads, details, threadLoads });
|
|
243
|
+
return thread;
|
|
244
|
+
}
|
|
245
|
+
setLoad(threadId, state) {
|
|
246
|
+
const threadLoads = new Map(this.snapshot.threadLoads);
|
|
247
|
+
threadLoads.set(threadId, state);
|
|
248
|
+
this.set({ ...this.snapshot, threadLoads });
|
|
249
|
+
}
|
|
250
|
+
remove(threadId) {
|
|
251
|
+
// A removal is an answer too: an older read landing after it is crossed, and reads again.
|
|
252
|
+
this.takenAt.set(threadId, ++this.taken);
|
|
253
|
+
const details = new Map(this.snapshot.details);
|
|
254
|
+
details.delete(threadId);
|
|
255
|
+
this.set({
|
|
256
|
+
...this.snapshot,
|
|
257
|
+
threads: this.snapshot.threads.filter((thread) => thread.id !== threadId),
|
|
258
|
+
details,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
set(snapshot) {
|
|
262
|
+
this.snapshot = snapshot;
|
|
263
|
+
this.listeners.forEach((listener) => listener());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Subscribes through the SDK's websocket connection (the app channel the app already joined). */
|
|
267
|
+
export const subscribeWithWebsockets = (service) => (appId) => (onPing) => {
|
|
268
|
+
const callback = async (data) => onPing(data);
|
|
269
|
+
service.listen(CommentRealtimeEventTypes.COMMENTS_CHANGED, callback, { filter: { appId } });
|
|
270
|
+
return () => service.unlisten(CommentRealtimeEventTypes.COMMENTS_CHANGED, callback);
|
|
271
|
+
};
|
|
272
|
+
/** The app's comment pings, on the websocket connection the SDK opened for the app. */
|
|
273
|
+
export const commentPingsFor = (appId) => subscribeWithWebsockets(websocketsService)(appId);
|
|
274
|
+
/**
|
|
275
|
+
* The threads of the current page while `isActive` (comment mode on): nothing is loaded and nothing
|
|
276
|
+
* listened to otherwise.
|
|
277
|
+
*/
|
|
278
|
+
export const useCommentThreads = ({ api, appId, subscribe, routePath, isActive, }) => {
|
|
279
|
+
const store = useMemo(() => new CommentThreadsStore({ api, appId, subscribe }), [api, appId, subscribe]);
|
|
280
|
+
useEffect(() => () => store.dispose(), [store]);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
void store.setRoute(isActive ? routePath : null);
|
|
283
|
+
}, [store, isActive, routePath]);
|
|
284
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
285
|
+
return { store, snapshot };
|
|
286
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire shapes and values of in-app comments, as the activity service sends and takes them.
|
|
3
|
+
*
|
|
4
|
+
* A copy of `@blockscom/common`'s `types/comments/comments-api.ts`: this package is public and does
|
|
5
|
+
* not depend on that one. A spec here (`test/comments/wire-contract.spec.ts`) keeps the two the
|
|
6
|
+
* same: change both.
|
|
7
|
+
*/
|
|
8
|
+
/** The text that was highlighted, with a little context on each side (W3C TextQuoteSelector). */
|
|
9
|
+
export interface CommentAnchorText {
|
|
10
|
+
exact: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
suffix: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The element a comment points at. No single id survives an edit to the app (`data-dev-id` is
|
|
16
|
+
* `file:line:col`), so the anchor keeps several signals and the client tries them in order.
|
|
17
|
+
*/
|
|
18
|
+
export interface CommentAnchorElement {
|
|
19
|
+
/** `data-dev-id` when the comment was made: exact while the code hasn't moved. */
|
|
20
|
+
devId?: string;
|
|
21
|
+
/** `data-dev-path`: the source file the element is rendered from. */
|
|
22
|
+
devPath?: string;
|
|
23
|
+
/** `data-dev-name`: the JSX element or component name. */
|
|
24
|
+
devName?: string;
|
|
25
|
+
/** Which match it was among the root's elements with the same `devPath` + `devName`. */
|
|
26
|
+
ordinal?: number;
|
|
27
|
+
/** A CSS path from the anchor root (the row's element, or the body): the last resort. */
|
|
28
|
+
domPath?: string;
|
|
29
|
+
tagName?: string;
|
|
30
|
+
/** The element's text when the comment was made (whitespace collapsed, capped). */
|
|
31
|
+
textSnapshot?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The data row the element shows, when the app marks it (`itemAnchorProps` → `data-blocks-item`). */
|
|
34
|
+
export interface CommentAnchorItem {
|
|
35
|
+
/** The table as the app's code names it (`<Name>Entity.tableBlockId`). */
|
|
36
|
+
tableBlockId: string;
|
|
37
|
+
itemId: string;
|
|
38
|
+
/** The column the picked element shows, when the app tagged it (`fieldAnchorProps`). */
|
|
39
|
+
field?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Where a thread lives inside the app. Stored as the client sent it; resolved on every render. */
|
|
42
|
+
export interface CommentAnchor {
|
|
43
|
+
v: 1;
|
|
44
|
+
/** The page's pathname, e.g. `/orders/123`: no query, no hash. Threads are listed per pathname. */
|
|
45
|
+
routePath: string;
|
|
46
|
+
item?: CommentAnchorItem;
|
|
47
|
+
element?: CommentAnchorElement;
|
|
48
|
+
text?: CommentAnchorText;
|
|
49
|
+
/** Where in the element the comment was dropped, as fractions of its box (0..1). */
|
|
50
|
+
point?: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export type CommentThreadStatus = "open" | "resolved";
|
|
56
|
+
/**
|
|
57
|
+
* An agent's reply to a comment that mentioned it. The first three states are open: at most one
|
|
58
|
+
* open reply exists per thread, agent and person who asked.
|
|
59
|
+
*
|
|
60
|
+
* - `working`: the agent is on it (a progress line in the body when it sent one).
|
|
61
|
+
* - `needs_input`: the agent asked the person a question, answered in the thread.
|
|
62
|
+
* - `waiting_in_chat`: the run paused on an approval or a question, answered in the agent's chat.
|
|
63
|
+
* - `done`: the answer is in the body.
|
|
64
|
+
* - `failed`: the agent could not finish (its reason in the body when it gave one).
|
|
65
|
+
*/
|
|
66
|
+
export type CommentAgentReplyState = "working" | "needs_input" | "waiting_in_chat" | "done" | "failed";
|
|
67
|
+
export declare const OPEN_AGENT_REPLY_STATES: readonly CommentAgentReplyState[];
|
|
68
|
+
/** A `working` reply with no update for this long reads as `failed`: its run was lost. */
|
|
69
|
+
export declare const AGENT_REPLY_STALE_AFTER_MS: number;
|
|
70
|
+
export type CommentAuthorDto = {
|
|
71
|
+
type: "user";
|
|
72
|
+
userId: number;
|
|
73
|
+
} | {
|
|
74
|
+
type: "agent";
|
|
75
|
+
agentId: string;
|
|
76
|
+
requestedByUserId: number;
|
|
77
|
+
};
|
|
78
|
+
export interface CommentAgentReplyDto {
|
|
79
|
+
state: CommentAgentReplyState;
|
|
80
|
+
/**
|
|
81
|
+
* The agent's conversation for this thread with the person who asked (`author.requestedByUserId`):
|
|
82
|
+
* present for everyone who sees the reply. Everyone may read it; only the asker writes in it.
|
|
83
|
+
*/
|
|
84
|
+
agentChatThreadId?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A comment's conversation with an agent (`comment-<threadId>-u<askerUserId>`, as activity and
|
|
88
|
+
* wf-actions-py mint it) read back: the comment thread it is about and the person who asked. Null
|
|
89
|
+
* for any other chat thread id.
|
|
90
|
+
*/
|
|
91
|
+
export declare const parseCommentAgentChatThreadId: (chatThreadId: string | null | undefined) => {
|
|
92
|
+
threadId: number;
|
|
93
|
+
askerUserId: number;
|
|
94
|
+
} | null;
|
|
95
|
+
export interface CommentDto {
|
|
96
|
+
id: number;
|
|
97
|
+
threadId: number;
|
|
98
|
+
author: CommentAuthorDto;
|
|
99
|
+
/**
|
|
100
|
+
* Plain text. A person is mentioned as `@[<userId>]`, an agent as `@[agent:<agentId>]`. Empty
|
|
101
|
+
* when deleted.
|
|
102
|
+
*/
|
|
103
|
+
body: string;
|
|
104
|
+
mentionedUserIds: number[];
|
|
105
|
+
mentionedAgentIds: string[];
|
|
106
|
+
/** Present on an agent's reply. */
|
|
107
|
+
agentReply?: CommentAgentReplyDto;
|
|
108
|
+
createdAt: string;
|
|
109
|
+
editedAt: string | null;
|
|
110
|
+
deleted: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Someone with a comment in a thread: a person or an agent. */
|
|
113
|
+
export type CommentParticipantDto = {
|
|
114
|
+
type: "user";
|
|
115
|
+
userId: number;
|
|
116
|
+
} | {
|
|
117
|
+
type: "agent";
|
|
118
|
+
agentId: string;
|
|
119
|
+
};
|
|
120
|
+
export interface CommentThreadSummaryDto {
|
|
121
|
+
id: number;
|
|
122
|
+
appId: string;
|
|
123
|
+
routePath: string;
|
|
124
|
+
anchor: CommentAnchor;
|
|
125
|
+
status: CommentThreadStatus;
|
|
126
|
+
createdByUserId: number;
|
|
127
|
+
createdAt: string;
|
|
128
|
+
lastActivityAt: string;
|
|
129
|
+
commentCount: number;
|
|
130
|
+
/** The agent working on the thread right now (a `working` reply, not one waiting on a person): the pin's spinner. */
|
|
131
|
+
workingAgentId: string | null;
|
|
132
|
+
/** Everyone with a comment in the thread, by their first comment: the starter first. */
|
|
133
|
+
participants: CommentParticipantDto[];
|
|
134
|
+
}
|
|
135
|
+
export interface CommentThreadDto extends CommentThreadSummaryDto {
|
|
136
|
+
resolvedByUserId: number | null;
|
|
137
|
+
resolvedByAgentId: string | null;
|
|
138
|
+
resolvedAt: string | null;
|
|
139
|
+
/** Oldest first; the newest `COMMENT_THREAD_MAX_COMMENTS`. */
|
|
140
|
+
comments: CommentDto[];
|
|
141
|
+
}
|
|
142
|
+
export interface CommentThreadsPageDto {
|
|
143
|
+
threads: CommentThreadSummaryDto[];
|
|
144
|
+
/** Pass back as `cursor` for the next page; `null` on the last one. */
|
|
145
|
+
nextCursor: string | null;
|
|
146
|
+
}
|
|
147
|
+
/** Someone the caller can mention: an active member of the account who can open the app. */
|
|
148
|
+
export interface MentionableUserDto {
|
|
149
|
+
userId: number;
|
|
150
|
+
name: string;
|
|
151
|
+
email: string | null;
|
|
152
|
+
profileImageUrl: string | null;
|
|
153
|
+
}
|
|
154
|
+
export interface MentionableUsersDto {
|
|
155
|
+
users: MentionableUserDto[];
|
|
156
|
+
}
|
|
157
|
+
/** Limits, shared by the API's validation and the composer. */
|
|
158
|
+
export declare const COMMENT_BODY_MAX_LENGTH = 5000;
|
|
159
|
+
export declare const COMMENT_ANCHOR_TEXT_MAX_LENGTH = 1000;
|
|
160
|
+
export declare const COMMENT_ANCHOR_CONTEXT_MAX_LENGTH = 64;
|
|
161
|
+
/** A tagged column's name (`CommentAnchorItem.field`). */
|
|
162
|
+
export declare const COMMENT_ANCHOR_FIELD_MAX_LENGTH = 255;
|
|
163
|
+
export declare const COMMENT_ROUTE_PATH_MAX_LENGTH = 512;
|
|
164
|
+
export declare const COMMENT_MAX_USER_MENTIONS = 20;
|
|
165
|
+
export declare const COMMENT_MAX_AGENT_MENTIONS = 3;
|
|
166
|
+
export declare const COMMENT_THREADS_PAGE_SIZE = 100;
|
|
167
|
+
export declare const COMMENT_THREAD_MAX_COMMENTS = 200;
|
|
168
|
+
/** A person mentioned in a body: `@[42]`. */
|
|
169
|
+
export declare const COMMENT_USER_MENTION_PATTERN: RegExp;
|
|
170
|
+
/** An agent mentioned in a body: `@[agent:SupportAgent]` (the agent block's name). */
|
|
171
|
+
export declare const COMMENT_AGENT_MENTION_PATTERN: RegExp;
|
|
172
|
+
/** The people and agents a body mentions, deduplicated, in order of first mention. */
|
|
173
|
+
export declare const parseCommentMentions: (body: string) => {
|
|
174
|
+
userIds: number[];
|
|
175
|
+
agentIds: string[];
|
|
176
|
+
};
|
|
177
|
+
/** Pusher events activity sends on `private-app-<appId>`. */
|
|
178
|
+
export declare enum CommentRealtimeEventTypes {
|
|
179
|
+
COMMENTS_CHANGED = "COMMENTS_CHANGED"
|
|
180
|
+
}
|
|
181
|
+
export type CommentsChangedReason = "thread_created" | "created" | "edited" | "deleted" | "resolved" | "reopened" | "agent_reply";
|
|
182
|
+
/**
|
|
183
|
+
* `COMMENTS_CHANGED`: a thread changed. Ids only, never text or the page: Pusher is not where
|
|
184
|
+
* customer content goes (HIPAA), and an installed app's channel is shared by every account that
|
|
185
|
+
* installed it. Clients refetch the thread by id, which applies who may see it.
|
|
186
|
+
*/
|
|
187
|
+
export interface CommentsChangedPayload {
|
|
188
|
+
/** Comments are per account: a client ignores pings for an account it is not signed into. */
|
|
189
|
+
accountId: number;
|
|
190
|
+
appId: string;
|
|
191
|
+
threadId: number;
|
|
192
|
+
reason: CommentsChangedReason;
|
|
193
|
+
commentId?: number;
|
|
194
|
+
}
|
|
195
|
+
/** Notification `source`s for comments. */
|
|
196
|
+
export declare const COMMENT_NOTIFICATION_SOURCES: {
|
|
197
|
+
readonly mention: "comments.mention";
|
|
198
|
+
readonly reply: "comments.reply";
|
|
199
|
+
readonly agentReply: "comments.agent-reply";
|
|
200
|
+
};
|
|
201
|
+
/** The reply handler key of comment notifications: a reply typed into one is posted in its thread. */
|
|
202
|
+
export declare const COMMENT_REPLY_HANDLER_KEY = "comments.reply";
|
|
203
|
+
/** The query parameter that opens a thread when the app loads (notification links). */
|
|
204
|
+
export declare const COMMENT_THREAD_QUERY_PARAM = "comment";
|
|
205
|
+
/**
|
|
206
|
+
* What the agent is told about the place a comment (or a pick from the chat) points at. The same
|
|
207
|
+
* words wherever the agent hears it from: react-common has a twin, and a spec keeps them equal.
|
|
208
|
+
*/
|
|
209
|
+
export declare const describeAnchorForAgent: (anchor: CommentAnchor) => string;
|
|
210
|
+
/**
|
|
211
|
+
* A comment's message in an agent's chat: one leading line naming the comment, then its text.
|
|
212
|
+
* activity writes it when a comment starts or continues an agent's turn; the chat reads it to
|
|
213
|
+
* show the message as the comment it is (what it is on, a link back to the thread). The agent
|
|
214
|
+
* reads the line too.
|
|
215
|
+
*
|
|
216
|
+
* [Comment #7 on “Q4 invoice” · /tasks/board]
|
|
217
|
+
* @Ron why is this late?
|
|
218
|
+
*/
|
|
219
|
+
export interface CommentChatMessage {
|
|
220
|
+
threadId: number;
|
|
221
|
+
/** What the comment is on, short (`commentSubjectOf`). */
|
|
222
|
+
label: string;
|
|
223
|
+
/** The page the thread is on. */
|
|
224
|
+
routePath: string;
|
|
225
|
+
text: string;
|
|
226
|
+
}
|
|
227
|
+
/** What a comment is on, in a few words: its quote, else its element's text or name, else its page. */
|
|
228
|
+
export declare const commentSubjectOf: (anchor: CommentAnchor) => string;
|
|
229
|
+
export declare const commentChatText: (message: CommentChatMessage) => string;
|
|
230
|
+
/** A chat message's text as the comment it carries, or null when it isn't one. */
|
|
231
|
+
export declare const parseCommentChatPart: (text: string) => CommentChatMessage | null;
|
|
232
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/comments/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire shapes and values of in-app comments, as the activity service sends and takes them.
|
|
3
|
+
*
|
|
4
|
+
* A copy of `@blockscom/common`'s `types/comments/comments-api.ts`: this package is public and does
|
|
5
|
+
* not depend on that one. A spec here (`test/comments/wire-contract.spec.ts`) keeps the two the
|
|
6
|
+
* same: change both.
|
|
7
|
+
*/
|
|
8
|
+
export const OPEN_AGENT_REPLY_STATES = ["working", "needs_input", "waiting_in_chat"];
|
|
9
|
+
/** A `working` reply with no update for this long reads as `failed`: its run was lost. */
|
|
10
|
+
export const AGENT_REPLY_STALE_AFTER_MS = 30 * 60 * 1000;
|
|
11
|
+
const COMMENT_AGENT_CHAT_THREAD_PATTERN = /^comment-(\d{1,15})-u(\d{1,15})$/;
|
|
12
|
+
/**
|
|
13
|
+
* A comment's conversation with an agent (`comment-<threadId>-u<askerUserId>`, as activity and
|
|
14
|
+
* wf-actions-py mint it) read back: the comment thread it is about and the person who asked. Null
|
|
15
|
+
* for any other chat thread id.
|
|
16
|
+
*/
|
|
17
|
+
export const parseCommentAgentChatThreadId = (chatThreadId) => {
|
|
18
|
+
const match = chatThreadId ? COMMENT_AGENT_CHAT_THREAD_PATTERN.exec(chatThreadId) : null;
|
|
19
|
+
return match ? { threadId: Number(match[1]), askerUserId: Number(match[2]) } : null;
|
|
20
|
+
};
|
|
21
|
+
/** Limits, shared by the API's validation and the composer. */
|
|
22
|
+
export const COMMENT_BODY_MAX_LENGTH = 5_000;
|
|
23
|
+
export const COMMENT_ANCHOR_TEXT_MAX_LENGTH = 1_000;
|
|
24
|
+
export const COMMENT_ANCHOR_CONTEXT_MAX_LENGTH = 64;
|
|
25
|
+
/** A tagged column's name (`CommentAnchorItem.field`). */
|
|
26
|
+
export const COMMENT_ANCHOR_FIELD_MAX_LENGTH = 255;
|
|
27
|
+
export const COMMENT_ROUTE_PATH_MAX_LENGTH = 512;
|
|
28
|
+
export const COMMENT_MAX_USER_MENTIONS = 20;
|
|
29
|
+
export const COMMENT_MAX_AGENT_MENTIONS = 3;
|
|
30
|
+
export const COMMENT_THREADS_PAGE_SIZE = 100;
|
|
31
|
+
export const COMMENT_THREAD_MAX_COMMENTS = 200;
|
|
32
|
+
/** A person mentioned in a body: `@[42]`. */
|
|
33
|
+
export const COMMENT_USER_MENTION_PATTERN = /@\[(\d{1,12})\]/g;
|
|
34
|
+
/** An agent mentioned in a body: `@[agent:SupportAgent]` (the agent block's name). */
|
|
35
|
+
export const COMMENT_AGENT_MENTION_PATTERN = /@\[agent:([^\]\n]{1,255})\]/g;
|
|
36
|
+
/** The people and agents a body mentions, deduplicated, in order of first mention. */
|
|
37
|
+
export const parseCommentMentions = (body) => {
|
|
38
|
+
const userIds = new Set();
|
|
39
|
+
const agentIds = new Set();
|
|
40
|
+
for (const match of body.matchAll(COMMENT_USER_MENTION_PATTERN)) {
|
|
41
|
+
userIds.add(Number(match[1]));
|
|
42
|
+
}
|
|
43
|
+
for (const match of body.matchAll(COMMENT_AGENT_MENTION_PATTERN)) {
|
|
44
|
+
agentIds.add(match[1]);
|
|
45
|
+
}
|
|
46
|
+
return { userIds: [...userIds], agentIds: [...agentIds] };
|
|
47
|
+
};
|
|
48
|
+
/** Pusher events activity sends on `private-app-<appId>`. */
|
|
49
|
+
export var CommentRealtimeEventTypes;
|
|
50
|
+
(function (CommentRealtimeEventTypes) {
|
|
51
|
+
CommentRealtimeEventTypes["COMMENTS_CHANGED"] = "COMMENTS_CHANGED";
|
|
52
|
+
})(CommentRealtimeEventTypes || (CommentRealtimeEventTypes = {}));
|
|
53
|
+
/** Notification `source`s for comments. */
|
|
54
|
+
export const COMMENT_NOTIFICATION_SOURCES = {
|
|
55
|
+
mention: "comments.mention",
|
|
56
|
+
reply: "comments.reply",
|
|
57
|
+
agentReply: "comments.agent-reply",
|
|
58
|
+
};
|
|
59
|
+
/** The reply handler key of comment notifications: a reply typed into one is posted in its thread. */
|
|
60
|
+
export const COMMENT_REPLY_HANDLER_KEY = "comments.reply";
|
|
61
|
+
/** The query parameter that opens a thread when the app loads (notification links). */
|
|
62
|
+
export const COMMENT_THREAD_QUERY_PARAM = "comment";
|
|
63
|
+
const AGENT_TEXT_MAX = 300;
|
|
64
|
+
const clip = (text, max) => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
65
|
+
/**
|
|
66
|
+
* What the agent is told about the place a comment (or a pick from the chat) points at. The same
|
|
67
|
+
* words wherever the agent hears it from: react-common has a twin, and a spec keeps them equal.
|
|
68
|
+
*/
|
|
69
|
+
export const describeAnchorForAgent = (anchor) => {
|
|
70
|
+
const lines = [`Page: ${anchor.routePath}`];
|
|
71
|
+
const element = anchor.element;
|
|
72
|
+
if (element?.devName || element?.tagName) {
|
|
73
|
+
const name = element.devName ?? element.tagName.toLowerCase();
|
|
74
|
+
lines.push(element.textSnapshot ? `Element: ${name} ("${clip(element.textSnapshot, AGENT_TEXT_MAX)}")` : `Element: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
if (anchor.text?.exact) {
|
|
77
|
+
lines.push(`Quoted text: "${clip(anchor.text.exact, AGENT_TEXT_MAX)}"`);
|
|
78
|
+
}
|
|
79
|
+
if (anchor.item) {
|
|
80
|
+
lines.push(`Table: ${anchor.item.tableBlockId}`, `Row id: ${anchor.item.itemId}`);
|
|
81
|
+
if (anchor.item.field) {
|
|
82
|
+
lines.push(`Field: ${anchor.item.field}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push("The table and row id name that record: read or update it with your data tools.");
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
};
|
|
88
|
+
const COMMENT_CHAT_LABEL_MAX = 80;
|
|
89
|
+
const COMMENT_CHAT_PATTERN = /^\[Comment #(\d{1,20}) on “([^”\n]*)” · ([^\]\n]*)\]\n/;
|
|
90
|
+
/** What a comment is on, in a few words: its quote, else its element's text or name, else its page. */
|
|
91
|
+
export const commentSubjectOf = (anchor) => anchor.text?.exact || anchor.element?.textSnapshot || anchor.element?.devName || anchor.routePath;
|
|
92
|
+
export const commentChatText = (message) => {
|
|
93
|
+
const label = clip(message.label.replace(/\s+/g, " ").replace(/”/g, "'").trim(), COMMENT_CHAT_LABEL_MAX);
|
|
94
|
+
const routePath = message.routePath.replace(/[\]\r\n]/g, "");
|
|
95
|
+
return `[Comment #${message.threadId} on “${label}” · ${routePath}]\n${message.text}`;
|
|
96
|
+
};
|
|
97
|
+
/** A chat message's text as the comment it carries, or null when it isn't one. */
|
|
98
|
+
export const parseCommentChatPart = (text) => {
|
|
99
|
+
const match = COMMENT_CHAT_PATTERN.exec(text);
|
|
100
|
+
if (!match) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
threadId: Number(match[1]),
|
|
105
|
+
label: match[2],
|
|
106
|
+
routePath: match[3],
|
|
107
|
+
text: text.slice(match[0].length),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -10,9 +10,20 @@ export interface AgentConversationContextValue {
|
|
|
10
10
|
isDraft: boolean;
|
|
11
11
|
/** The index under `filters`; its `renameThread` also keeps the open conversation's remembered title current. */
|
|
12
12
|
threads: UseAgentThreadsResult;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The open conversation's row: listed, else the last listed, else a stand-in (`isStandIn`), which
|
|
15
|
+
* shows the conversation's own title and status once they are read (`shownConversationRow`).
|
|
16
|
+
*/
|
|
14
17
|
row: AgentThreadItem;
|
|
15
18
|
isStandIn: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The open conversation's own read is done and the server has no row for it that this reader
|
|
21
|
+
* may see (a 404): someone else's comment conversation the reader can't see, or — briefly — a
|
|
22
|
+
* conversation just started whose row hasn't landed. A host tells the two apart (react-common's
|
|
23
|
+
* `parseCommentAgentChatThreadId`) before saying the conversation isn't available. False for a
|
|
24
|
+
* draft and while the read is in flight.
|
|
25
|
+
*/
|
|
26
|
+
isConversationMissing: boolean;
|
|
16
27
|
filters: AgentThreadFilters;
|
|
17
28
|
setFilters: (filters: AgentThreadFilters) => void;
|
|
18
29
|
hasActiveFilters: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-conversation-context.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-conversation-context.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"agent-conversation-context.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-conversation-context.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAGzB,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAmB,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE/G,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAMnF,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,OAAO,EAAE,qBAAqB,CAAC;IAC/B;;;OAGG;IACH,GAAG,EAAE,eAAe,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,qBAAqB,EAAE,OAAO,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,UAAU,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAClD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uFAAuF;IACvF,iBAAiB,EAAE,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;CACpE;AAID,MAAM,WAAW,8BAA+B,SAAQ,IAAI,CAC1D,sBAAsB,EACtB,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CACvD;IACC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,QAAQ,GACT,EAAE,8BAA8B,+BA+BhC;AAED,MAAM,MAAM,oCAAoC,GAAG,8BAA8B,CAAC;AAKlF;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,oCAAoC,+BAgBtC;AAiDD,eAAO,MAAM,2BAA2B,QAAO,6BAQ9C,CAAC"}
|