@maintainer-pro/ai-ui 0.1.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/ai-ui.css +2 -0
- package/dist/ai-ui.iife.js +63 -0
- package/dist/ai-ui.iife.js.map +1 -0
- package/dist/index.cjs +2097 -0
- package/dist/index.d.cts +181 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.js +2071 -0
- package/embed.d.ts +75 -0
- package/package.json +59 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AiChat: () => AiChat,
|
|
34
|
+
buildBrowserClientContext: () => buildBrowserClientContext,
|
|
35
|
+
countUnreadSince: () => countUnreadSince,
|
|
36
|
+
createMaintainerProClient: () => createMaintainerProClient,
|
|
37
|
+
fetchChatMessageTimestamps: () => fetchChatMessageTimestamps,
|
|
38
|
+
fetchChatMessages: () => fetchChatMessages,
|
|
39
|
+
rememberConversationId: () => rememberConversationId,
|
|
40
|
+
startChatUnreadPoller: () => startChatUnreadPoller,
|
|
41
|
+
useChatUnread: () => useChatUnread,
|
|
42
|
+
useClientContextBuilder: () => useClientContextBuilder,
|
|
43
|
+
usePersistedChatOpen: () => usePersistedChatOpen
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/AiChat.tsx
|
|
48
|
+
var import_react2 = require("react");
|
|
49
|
+
var import_react3 = require("@assistant-ui/react");
|
|
50
|
+
var import_react_markdown = require("@assistant-ui/react-markdown");
|
|
51
|
+
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
52
|
+
|
|
53
|
+
// src/context.ts
|
|
54
|
+
var import_react = require("react");
|
|
55
|
+
function buildBrowserClientContext(input = {}) {
|
|
56
|
+
const route = input.route ?? (typeof window !== "undefined" ? window.location.pathname : "/");
|
|
57
|
+
const pageTitle = input.pageTitle ?? (typeof document !== "undefined" ? document.title || "App" : "App");
|
|
58
|
+
const focused = typeof document !== "undefined" && document.activeElement instanceof HTMLElement ? document.activeElement.innerText?.slice(0, 80) || document.activeElement.getAttribute("aria-label") || document.activeElement.tagName.toLowerCase() : null;
|
|
59
|
+
return {
|
|
60
|
+
route,
|
|
61
|
+
pageTitle,
|
|
62
|
+
visiblePanels: input.visiblePanels,
|
|
63
|
+
focusedElement: focused,
|
|
64
|
+
relevantFiles: input.relevantFiles,
|
|
65
|
+
data: input.data
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function useClientContextBuilder(getInput) {
|
|
69
|
+
return (0, import_react.useCallback)(() => buildBrowserClientContext(getInput()), [getInput]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/maintainer-pro.ts
|
|
73
|
+
function joinUrl(baseUrl, path) {
|
|
74
|
+
return `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
75
|
+
}
|
|
76
|
+
function createMaintainerProClient(options) {
|
|
77
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
78
|
+
async function request(path, init) {
|
|
79
|
+
const res = await fetchImpl(joinUrl(options.baseUrl, path), {
|
|
80
|
+
...init,
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
...init?.headers ?? {}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const text = await res.text().catch(() => "");
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Maintainer Pro ${path} failed (${res.status}): ${text || res.statusText}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return await res.json();
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
async getApplication() {
|
|
97
|
+
const data = await request(
|
|
98
|
+
"/api/v1/client/application"
|
|
99
|
+
);
|
|
100
|
+
return data.application;
|
|
101
|
+
},
|
|
102
|
+
async listConversations() {
|
|
103
|
+
const data = await request("/api/v1/store/conversations");
|
|
104
|
+
return data.conversations;
|
|
105
|
+
},
|
|
106
|
+
async listChatUsers() {
|
|
107
|
+
const data = await request(
|
|
108
|
+
"/api/v1/client/chat-users"
|
|
109
|
+
);
|
|
110
|
+
return data.users ?? [];
|
|
111
|
+
},
|
|
112
|
+
async loginChatUser(email, password) {
|
|
113
|
+
const res = await fetchImpl(
|
|
114
|
+
joinUrl(options.baseUrl, "/api/v1/client/chat-login"),
|
|
115
|
+
{
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
119
|
+
"Content-Type": "application/json"
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ email, password })
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
if (res.status === 401) {
|
|
126
|
+
throw new Error("Invalid email or password");
|
|
127
|
+
}
|
|
128
|
+
if (res.status === 403) {
|
|
129
|
+
throw new Error("Account disabled");
|
|
130
|
+
}
|
|
131
|
+
const text = await res.text().catch(() => "");
|
|
132
|
+
throw new Error(text || `Sign in failed (${res.status})`);
|
|
133
|
+
}
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
return data.user;
|
|
136
|
+
},
|
|
137
|
+
async listMessages(conversationId) {
|
|
138
|
+
const data = await request(
|
|
139
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/messages`
|
|
140
|
+
);
|
|
141
|
+
return data.messages ?? [];
|
|
142
|
+
},
|
|
143
|
+
attachmentUrl(attachmentId) {
|
|
144
|
+
return joinUrl(
|
|
145
|
+
options.baseUrl,
|
|
146
|
+
`/api/v1/store/attachments/${encodeURIComponent(attachmentId)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/chat-unread.ts
|
|
153
|
+
function defaultStorageKey(conversationId) {
|
|
154
|
+
return `ai-ui:chat-read:${conversationId ?? "default"}`;
|
|
155
|
+
}
|
|
156
|
+
function resolveUnreadStorageKey(options) {
|
|
157
|
+
return options.storageKey ?? defaultStorageKey(options.conversationId);
|
|
158
|
+
}
|
|
159
|
+
function readLastReadAt(storageKey) {
|
|
160
|
+
if (typeof window === "undefined") return null;
|
|
161
|
+
try {
|
|
162
|
+
return window.localStorage.getItem(storageKey);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function writeLastReadAt(storageKey, iso) {
|
|
168
|
+
if (typeof window === "undefined") return;
|
|
169
|
+
try {
|
|
170
|
+
window.localStorage.setItem(storageKey, iso);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function mpCacheKey(baseUrl, apiKey) {
|
|
175
|
+
return `${baseUrl.replace(/\/$/, "")}|${apiKey}`;
|
|
176
|
+
}
|
|
177
|
+
var resolvedConversationIds = /* @__PURE__ */ new Map();
|
|
178
|
+
function conversationIdStorageKey(baseUrl, apiKey) {
|
|
179
|
+
return `ai-ui:mp-conversation:${mpCacheKey(baseUrl, apiKey)}`;
|
|
180
|
+
}
|
|
181
|
+
function readCachedConversationId(baseUrl, apiKey) {
|
|
182
|
+
const key = mpCacheKey(baseUrl, apiKey);
|
|
183
|
+
const mem = resolvedConversationIds.get(key);
|
|
184
|
+
if (mem) return mem;
|
|
185
|
+
if (typeof window === "undefined") return null;
|
|
186
|
+
try {
|
|
187
|
+
return window.sessionStorage.getItem(
|
|
188
|
+
conversationIdStorageKey(baseUrl, apiKey)
|
|
189
|
+
);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function rememberConversationId(baseUrl, apiKey, conversationId) {
|
|
195
|
+
if (!conversationId || conversationId === "shared") return;
|
|
196
|
+
const key = mpCacheKey(baseUrl, apiKey);
|
|
197
|
+
resolvedConversationIds.set(key, conversationId);
|
|
198
|
+
if (typeof window === "undefined") return;
|
|
199
|
+
try {
|
|
200
|
+
window.sessionStorage.setItem(
|
|
201
|
+
conversationIdStorageKey(baseUrl, apiKey),
|
|
202
|
+
conversationId
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function resolveMaintainerProConversationId(baseUrl, apiKey, conversationId) {
|
|
208
|
+
if (conversationId && conversationId !== "shared") {
|
|
209
|
+
rememberConversationId(baseUrl, apiKey, conversationId);
|
|
210
|
+
return conversationId;
|
|
211
|
+
}
|
|
212
|
+
const cached = readCachedConversationId(baseUrl, apiKey);
|
|
213
|
+
if (cached) return cached;
|
|
214
|
+
const mp = createMaintainerProClient({ baseUrl, apiKey });
|
|
215
|
+
const conversations = await mp.listConversations();
|
|
216
|
+
const id = conversations[0]?.id ?? null;
|
|
217
|
+
if (id) rememberConversationId(baseUrl, apiKey, id);
|
|
218
|
+
return id;
|
|
219
|
+
}
|
|
220
|
+
async function fetchChatMessages(options) {
|
|
221
|
+
const {
|
|
222
|
+
apiUrl = "/api/chat",
|
|
223
|
+
maintainerProUrl,
|
|
224
|
+
maintainerProApiKey,
|
|
225
|
+
conversationId
|
|
226
|
+
} = options;
|
|
227
|
+
if (maintainerProUrl && maintainerProApiKey) {
|
|
228
|
+
const id = await resolveMaintainerProConversationId(
|
|
229
|
+
maintainerProUrl,
|
|
230
|
+
maintainerProApiKey,
|
|
231
|
+
conversationId
|
|
232
|
+
);
|
|
233
|
+
if (!id) return [];
|
|
234
|
+
const mp = createMaintainerProClient({
|
|
235
|
+
baseUrl: maintainerProUrl,
|
|
236
|
+
apiKey: maintainerProApiKey
|
|
237
|
+
});
|
|
238
|
+
const messages = await mp.listMessages(id);
|
|
239
|
+
return messages.map((m) => ({
|
|
240
|
+
id: m.id,
|
|
241
|
+
role: m.role,
|
|
242
|
+
content: m.content ?? "",
|
|
243
|
+
provider: m.provider,
|
|
244
|
+
senderType: m.senderType,
|
|
245
|
+
senderName: m.senderName,
|
|
246
|
+
createdAt: m.createdAt
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
const res = await fetch(apiUrl);
|
|
250
|
+
if (!res.ok) return [];
|
|
251
|
+
const data = await res.json().catch(() => ({}));
|
|
252
|
+
return (data.messages ?? []).map((m) => ({
|
|
253
|
+
id: m.id,
|
|
254
|
+
role: m.role,
|
|
255
|
+
content: m.content ?? "",
|
|
256
|
+
provider: m.provider,
|
|
257
|
+
senderType: m.senderType,
|
|
258
|
+
senderName: m.senderName,
|
|
259
|
+
createdAt: m.createdAt
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
async function fetchChatMessageTimestamps(options) {
|
|
263
|
+
const {
|
|
264
|
+
apiUrl = "/api/chat",
|
|
265
|
+
maintainerProUrl,
|
|
266
|
+
maintainerProApiKey,
|
|
267
|
+
conversationId
|
|
268
|
+
} = options;
|
|
269
|
+
if (maintainerProUrl && maintainerProApiKey) {
|
|
270
|
+
const cached = readCachedConversationId(
|
|
271
|
+
maintainerProUrl,
|
|
272
|
+
maintainerProApiKey
|
|
273
|
+
);
|
|
274
|
+
const knownId = conversationId && conversationId !== "shared" ? conversationId : cached;
|
|
275
|
+
if (knownId) {
|
|
276
|
+
const messages = await fetchChatMessages({
|
|
277
|
+
...options,
|
|
278
|
+
conversationId: knownId
|
|
279
|
+
});
|
|
280
|
+
return messages.map((m) => m.createdAt).filter((t) => Boolean(t));
|
|
281
|
+
}
|
|
282
|
+
const mp = createMaintainerProClient({
|
|
283
|
+
baseUrl: maintainerProUrl,
|
|
284
|
+
apiKey: maintainerProApiKey
|
|
285
|
+
});
|
|
286
|
+
const conversations = await mp.listConversations();
|
|
287
|
+
const first = conversations[0];
|
|
288
|
+
if (first?.id) {
|
|
289
|
+
rememberConversationId(
|
|
290
|
+
maintainerProUrl,
|
|
291
|
+
maintainerProApiKey,
|
|
292
|
+
first.id
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return (first?.messages ?? []).map((m) => m.createdAt).filter((t) => Boolean(t));
|
|
296
|
+
}
|
|
297
|
+
const res = await fetch(apiUrl);
|
|
298
|
+
if (!res.ok) return [];
|
|
299
|
+
const data = await res.json().catch(() => ({}));
|
|
300
|
+
return (data.messages ?? []).map((m) => m.createdAt).filter((t) => Boolean(t));
|
|
301
|
+
}
|
|
302
|
+
function countUnreadSince(timestamps, lastReadAt) {
|
|
303
|
+
if (!lastReadAt) return 0;
|
|
304
|
+
const watermark = Date.parse(lastReadAt);
|
|
305
|
+
if (Number.isNaN(watermark)) return 0;
|
|
306
|
+
return timestamps.filter((t) => {
|
|
307
|
+
const ms = Date.parse(t);
|
|
308
|
+
return !Number.isNaN(ms) && ms > watermark;
|
|
309
|
+
}).length;
|
|
310
|
+
}
|
|
311
|
+
function latestTimestamp(timestamps) {
|
|
312
|
+
let latest = null;
|
|
313
|
+
let latestMs = -Infinity;
|
|
314
|
+
for (const t of timestamps) {
|
|
315
|
+
const ms = Date.parse(t);
|
|
316
|
+
if (!Number.isNaN(ms) && ms >= latestMs) {
|
|
317
|
+
latestMs = ms;
|
|
318
|
+
latest = t;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return latest;
|
|
322
|
+
}
|
|
323
|
+
function startChatUnreadPoller(options) {
|
|
324
|
+
const storageKey = resolveUnreadStorageKey(options);
|
|
325
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5e3;
|
|
326
|
+
let stopped = false;
|
|
327
|
+
let timer = null;
|
|
328
|
+
const setCount = (count) => {
|
|
329
|
+
if (!stopped) options.onChange(count);
|
|
330
|
+
};
|
|
331
|
+
const refresh = async () => {
|
|
332
|
+
if (stopped) return;
|
|
333
|
+
if (options.isOpen()) {
|
|
334
|
+
setCount(0);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const timestamps = await fetchChatMessageTimestamps(options);
|
|
339
|
+
const latest = latestTimestamp(timestamps);
|
|
340
|
+
let lastRead = readLastReadAt(storageKey);
|
|
341
|
+
if (!lastRead) {
|
|
342
|
+
lastRead = latest ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
writeLastReadAt(storageKey, lastRead);
|
|
344
|
+
setCount(0);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
setCount(countUnreadSince(timestamps, lastRead));
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const markRead = async () => {
|
|
352
|
+
setCount(0);
|
|
353
|
+
try {
|
|
354
|
+
const timestamps = await fetchChatMessageTimestamps(options);
|
|
355
|
+
const latest = latestTimestamp(timestamps) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
356
|
+
writeLastReadAt(storageKey, latest);
|
|
357
|
+
} catch {
|
|
358
|
+
writeLastReadAt(storageKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
void refresh();
|
|
362
|
+
timer = setInterval(() => {
|
|
363
|
+
void refresh();
|
|
364
|
+
}, pollIntervalMs);
|
|
365
|
+
return {
|
|
366
|
+
stop: () => {
|
|
367
|
+
stopped = true;
|
|
368
|
+
if (timer) clearInterval(timer);
|
|
369
|
+
timer = null;
|
|
370
|
+
},
|
|
371
|
+
refresh,
|
|
372
|
+
markRead
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/AiChat.tsx
|
|
377
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
378
|
+
function userBadge(user) {
|
|
379
|
+
if (user.role?.trim()) return user.role.trim();
|
|
380
|
+
if (user.type === "ai") return "ai";
|
|
381
|
+
return user.type;
|
|
382
|
+
}
|
|
383
|
+
var MessageImagesContext = (0, import_react2.createContext)(null);
|
|
384
|
+
var MessageSendersContext = (0, import_react2.createContext)(null);
|
|
385
|
+
var AiUserContext = (0, import_react2.createContext)(null);
|
|
386
|
+
var ChatUsersContext = (0, import_react2.createContext)([]);
|
|
387
|
+
function isComposableUser(user) {
|
|
388
|
+
return user.type === "client" || user.type === "developer";
|
|
389
|
+
}
|
|
390
|
+
function escapeRegExp(value) {
|
|
391
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
392
|
+
}
|
|
393
|
+
function getActiveMention(text, cursor) {
|
|
394
|
+
const before = text.slice(0, cursor);
|
|
395
|
+
const match = before.match(/@([\w.-]*)$/);
|
|
396
|
+
if (!match) return null;
|
|
397
|
+
return {
|
|
398
|
+
start: cursor - match[0].length,
|
|
399
|
+
query: match[1] ?? ""
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function MentionText({
|
|
403
|
+
text,
|
|
404
|
+
tone
|
|
405
|
+
}) {
|
|
406
|
+
const users = (0, import_react2.useContext)(ChatUsersContext);
|
|
407
|
+
const names = [
|
|
408
|
+
...new Set(users.map((u) => u.name.trim()).filter(Boolean))
|
|
409
|
+
].sort((a, b) => b.length - a.length);
|
|
410
|
+
if (names.length === 0 || !text) {
|
|
411
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: text });
|
|
412
|
+
}
|
|
413
|
+
const pattern = new RegExp(
|
|
414
|
+
`(@(?:${names.map(escapeRegExp).join("|")}))(?=$|\\s|[.,!?;:'")\\]])`,
|
|
415
|
+
"g"
|
|
416
|
+
);
|
|
417
|
+
const nodes = [];
|
|
418
|
+
let lastIndex = 0;
|
|
419
|
+
let match;
|
|
420
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
421
|
+
if (match.index > lastIndex) {
|
|
422
|
+
nodes.push(text.slice(lastIndex, match.index));
|
|
423
|
+
}
|
|
424
|
+
nodes.push(
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
426
|
+
"span",
|
|
427
|
+
{
|
|
428
|
+
className: tone === "user" ? "font-semibold text-white underline decoration-white/50 underline-offset-2" : "font-semibold text-indigo-600",
|
|
429
|
+
children: match[1]
|
|
430
|
+
},
|
|
431
|
+
`${match.index}-${match[1]}`
|
|
432
|
+
)
|
|
433
|
+
);
|
|
434
|
+
lastIndex = match.index + match[0].length;
|
|
435
|
+
}
|
|
436
|
+
if (lastIndex < text.length) {
|
|
437
|
+
nodes.push(text.slice(lastIndex));
|
|
438
|
+
}
|
|
439
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: nodes });
|
|
440
|
+
}
|
|
441
|
+
function userKey(user, index) {
|
|
442
|
+
return `${user.type}:${user.name}:${index}`;
|
|
443
|
+
}
|
|
444
|
+
function providerDisplayName(providerId) {
|
|
445
|
+
const id = (providerId ?? "").toLowerCase().trim();
|
|
446
|
+
if (!id || id === "developer") return null;
|
|
447
|
+
if (id === "claude") return "claude";
|
|
448
|
+
if (id === "cursor") return "cursor";
|
|
449
|
+
if (id === "antigravity" || id === "agy") return "antigravity";
|
|
450
|
+
return providerId.trim();
|
|
451
|
+
}
|
|
452
|
+
function resolveAiUser(users, providerId) {
|
|
453
|
+
const fromList = users?.find((u) => u.type === "ai");
|
|
454
|
+
const fromListName = fromList?.name.trim();
|
|
455
|
+
return {
|
|
456
|
+
type: "ai",
|
|
457
|
+
name: fromListName || providerDisplayName(providerId) || "ai"
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function senderFromStoredMessage(m, aiUser) {
|
|
461
|
+
if (m.senderType === "developer" || m.provider === "developer") {
|
|
462
|
+
return {
|
|
463
|
+
name: m.senderName?.trim() || "Developer",
|
|
464
|
+
type: "developer"
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (m.senderType === "client" || m.role === "user" && m.senderName) {
|
|
468
|
+
return {
|
|
469
|
+
name: m.senderName?.trim() || "Client",
|
|
470
|
+
type: "client"
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (m.role === "assistant" && (m.senderType === "ai" || m.provider && m.provider !== "developer" || m.senderType == null)) {
|
|
474
|
+
return {
|
|
475
|
+
type: "ai",
|
|
476
|
+
name: m.senderName?.trim() || providerDisplayName(m.provider) || aiUser.name
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
function persistDeveloperReply(apiUrl, conversationId, content, senderName) {
|
|
482
|
+
if (!conversationId || !content.trim()) return;
|
|
483
|
+
void fetch(apiUrl, {
|
|
484
|
+
method: "POST",
|
|
485
|
+
headers: { "Content-Type": "application/json" },
|
|
486
|
+
body: JSON.stringify({
|
|
487
|
+
type: "reply",
|
|
488
|
+
conversationId,
|
|
489
|
+
content,
|
|
490
|
+
senderName,
|
|
491
|
+
senderType: "developer"
|
|
492
|
+
})
|
|
493
|
+
}).catch(() => {
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
var MAX_IMAGES = 5;
|
|
497
|
+
var MAX_BYTES = 4 * 1024 * 1024;
|
|
498
|
+
function fileToPending(file) {
|
|
499
|
+
return new Promise((resolve, reject) => {
|
|
500
|
+
if (!file.type.startsWith("image/")) {
|
|
501
|
+
reject(new Error("Only image files are supported"));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (file.size > MAX_BYTES) {
|
|
505
|
+
reject(new Error("Screenshot too large (max 4MB)"));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const reader = new FileReader();
|
|
509
|
+
reader.onload = () => {
|
|
510
|
+
const dataUrl = String(reader.result ?? "");
|
|
511
|
+
const base64 = dataUrl.includes(",") ? dataUrl.split(",")[1] ?? "" : dataUrl;
|
|
512
|
+
resolve({
|
|
513
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
514
|
+
name: file.name || "screenshot.png",
|
|
515
|
+
mimeType: file.type || "image/png",
|
|
516
|
+
dataUrl,
|
|
517
|
+
base64
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
reader.onerror = () => reject(new Error("Failed to read image"));
|
|
521
|
+
reader.readAsDataURL(file);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function getMessageText(message) {
|
|
525
|
+
return message.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
|
|
526
|
+
}
|
|
527
|
+
function takeSnapshot(captureState) {
|
|
528
|
+
if (!captureState) return void 0;
|
|
529
|
+
try {
|
|
530
|
+
return structuredClone(captureState());
|
|
531
|
+
} catch {
|
|
532
|
+
return captureState();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function combineUserPrompts(previous, next) {
|
|
536
|
+
const a = previous.trim();
|
|
537
|
+
const b = next.trim();
|
|
538
|
+
if (!a) return b;
|
|
539
|
+
if (!b) return a;
|
|
540
|
+
if (b.startsWith(a)) return b;
|
|
541
|
+
if (a.includes(b)) return a;
|
|
542
|
+
return `${a}
|
|
543
|
+
|
|
544
|
+
Also: ${b}`;
|
|
545
|
+
}
|
|
546
|
+
function isLikelyStatusAck(content) {
|
|
547
|
+
const text = content.trim();
|
|
548
|
+
if (!text || text.length > 96) return false;
|
|
549
|
+
return /^(on it|sure|yep|got it|okay|ok|alright|one sec|hang on)/i.test(
|
|
550
|
+
text
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
function prepareApiMessages(messages) {
|
|
554
|
+
const usable = messages.filter((m) => {
|
|
555
|
+
if (m.role !== "assistant") return true;
|
|
556
|
+
const status = m.status?.type;
|
|
557
|
+
return status !== "incomplete" && status !== "running";
|
|
558
|
+
});
|
|
559
|
+
const mapped = usable.map((m) => ({
|
|
560
|
+
role: m.role,
|
|
561
|
+
content: getMessageText(m)
|
|
562
|
+
}));
|
|
563
|
+
const trailingUsers = [];
|
|
564
|
+
while (mapped.length > 0) {
|
|
565
|
+
const last = mapped[mapped.length - 1];
|
|
566
|
+
if (last.role === "user") {
|
|
567
|
+
trailingUsers.unshift(mapped.pop().content);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (trailingUsers.length > 0 && isLikelyStatusAck(last.content)) {
|
|
571
|
+
mapped.pop();
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
if (trailingUsers.length > 0) {
|
|
577
|
+
mapped.push({
|
|
578
|
+
role: "user",
|
|
579
|
+
content: trailingUsers.reduce(
|
|
580
|
+
(prev, next) => combineUserPrompts(prev, next)
|
|
581
|
+
)
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return mapped;
|
|
585
|
+
}
|
|
586
|
+
function pickOne(options) {
|
|
587
|
+
return options[Math.floor(Math.random() * options.length)];
|
|
588
|
+
}
|
|
589
|
+
function buildInterimReply(messages, hasAttachments) {
|
|
590
|
+
if (hasAttachments) {
|
|
591
|
+
return pickOne([
|
|
592
|
+
"Got the screenshot \u2014 taking a look.",
|
|
593
|
+
"One sec, checking the image.",
|
|
594
|
+
"Alright, looking at what you sent."
|
|
595
|
+
]);
|
|
596
|
+
}
|
|
597
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
598
|
+
const text = lastUser ? getMessageText(lastUser).toLowerCase() : "";
|
|
599
|
+
if (/\b(create|add|new)\b/.test(text)) {
|
|
600
|
+
return pickOne([
|
|
601
|
+
"Sure, adding that now.",
|
|
602
|
+
"On it \u2014 making that for you.",
|
|
603
|
+
"Yep, give me a second."
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
if (/\b(update|edit|change|rename)\b/.test(text)) {
|
|
607
|
+
return pickOne([
|
|
608
|
+
"Okay, updating that.",
|
|
609
|
+
"Got it \u2014 changing it now.",
|
|
610
|
+
"Sure, one sec."
|
|
611
|
+
]);
|
|
612
|
+
}
|
|
613
|
+
if (/\b(delete|remove)\b/.test(text)) {
|
|
614
|
+
return pickOne([
|
|
615
|
+
"Okay, removing that.",
|
|
616
|
+
"Sure, I'll take that out.",
|
|
617
|
+
"On it."
|
|
618
|
+
]);
|
|
619
|
+
}
|
|
620
|
+
return pickOne([
|
|
621
|
+
"On it.",
|
|
622
|
+
"Sure, one sec.",
|
|
623
|
+
"Got it \u2014 hang on.",
|
|
624
|
+
"Yep, working on that."
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
function wait(ms, signal) {
|
|
628
|
+
return new Promise((resolve, reject) => {
|
|
629
|
+
if (signal?.aborted) {
|
|
630
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const timer = setTimeout(resolve, ms);
|
|
634
|
+
signal?.addEventListener(
|
|
635
|
+
"abort",
|
|
636
|
+
() => {
|
|
637
|
+
clearTimeout(timer);
|
|
638
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
639
|
+
},
|
|
640
|
+
{ once: true }
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function persistStatusMessage(apiUrl, conversationId, content) {
|
|
645
|
+
if (!conversationId || !content.trim()) return;
|
|
646
|
+
void fetch(apiUrl, {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "Content-Type": "application/json" },
|
|
649
|
+
body: JSON.stringify({
|
|
650
|
+
type: "status",
|
|
651
|
+
conversationId,
|
|
652
|
+
content
|
|
653
|
+
})
|
|
654
|
+
}).catch(() => {
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function useStableAdapter(options) {
|
|
658
|
+
const optsRef = (0, import_react2.useRef)(options);
|
|
659
|
+
optsRef.current = options;
|
|
660
|
+
return (0, import_react2.useMemo)(
|
|
661
|
+
() => ({
|
|
662
|
+
async *run({ messages, abortSignal }) {
|
|
663
|
+
const {
|
|
664
|
+
apiUrl,
|
|
665
|
+
buildContext,
|
|
666
|
+
onToolCalls,
|
|
667
|
+
captureState,
|
|
668
|
+
restoreState,
|
|
669
|
+
conversationId,
|
|
670
|
+
takePending,
|
|
671
|
+
registerImages,
|
|
672
|
+
registerSender,
|
|
673
|
+
getSelectedUser,
|
|
674
|
+
onProvider,
|
|
675
|
+
turnRef,
|
|
676
|
+
appendAssistant
|
|
677
|
+
} = optsRef.current;
|
|
678
|
+
turnRef.current = {
|
|
679
|
+
snapshot: takeSnapshot(captureState),
|
|
680
|
+
toolsApplied: false
|
|
681
|
+
};
|
|
682
|
+
const selectedUser = getSelectedUser();
|
|
683
|
+
const pending = takePending();
|
|
684
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
685
|
+
if (lastUser?.id && pending.length > 0) {
|
|
686
|
+
registerImages(
|
|
687
|
+
lastUser.id,
|
|
688
|
+
pending.map((img) => img.dataUrl)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (lastUser?.id && selectedUser) {
|
|
692
|
+
registerSender(lastUser.id, {
|
|
693
|
+
name: selectedUser.name,
|
|
694
|
+
type: selectedUser.type
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const apiMessages = prepareApiMessages(messages);
|
|
698
|
+
const interimText = buildInterimReply(messages, pending.length > 0);
|
|
699
|
+
const rawUserMessage = lastUser ? getMessageText(lastUser) : "";
|
|
700
|
+
const attachments = pending.map((img) => ({
|
|
701
|
+
name: img.name,
|
|
702
|
+
mimeType: img.mimeType,
|
|
703
|
+
data: img.base64
|
|
704
|
+
}));
|
|
705
|
+
const request = (async () => {
|
|
706
|
+
const res = await fetch(apiUrl, {
|
|
707
|
+
method: "POST",
|
|
708
|
+
headers: { "Content-Type": "application/json" },
|
|
709
|
+
body: JSON.stringify({
|
|
710
|
+
messages: apiMessages,
|
|
711
|
+
userMessage: rawUserMessage || void 0,
|
|
712
|
+
context: buildContext(),
|
|
713
|
+
conversationId,
|
|
714
|
+
attachments: attachments.length > 0 ? attachments : void 0,
|
|
715
|
+
senderType: selectedUser?.type === "client" ? "client" : void 0,
|
|
716
|
+
senderName: selectedUser?.name
|
|
717
|
+
}),
|
|
718
|
+
signal: abortSignal
|
|
719
|
+
});
|
|
720
|
+
const data2 = await res.json();
|
|
721
|
+
if (data2.superseded) {
|
|
722
|
+
return data2;
|
|
723
|
+
}
|
|
724
|
+
if (!res.ok) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
data2.error ?? `Failed to fetch from ${apiUrl} (${res.status})`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return data2;
|
|
730
|
+
})();
|
|
731
|
+
const delayMs = 1600 + Math.floor(Math.random() * 900);
|
|
732
|
+
let raced = "done";
|
|
733
|
+
try {
|
|
734
|
+
raced = await Promise.race([
|
|
735
|
+
request.then(() => "done"),
|
|
736
|
+
wait(delayMs, abortSignal).then(() => "delay")
|
|
737
|
+
]);
|
|
738
|
+
} catch {
|
|
739
|
+
raced = "aborted";
|
|
740
|
+
}
|
|
741
|
+
if (abortSignal.aborted || raced === "aborted") {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let showedInterim = false;
|
|
745
|
+
if (raced === "delay") {
|
|
746
|
+
showedInterim = true;
|
|
747
|
+
yield {
|
|
748
|
+
content: [{ type: "text", text: interimText }]
|
|
749
|
+
};
|
|
750
|
+
persistStatusMessage(apiUrl, conversationId, interimText);
|
|
751
|
+
}
|
|
752
|
+
if (abortSignal.aborted) return;
|
|
753
|
+
let data;
|
|
754
|
+
try {
|
|
755
|
+
data = await request;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
if (abortSignal.aborted) return;
|
|
758
|
+
const message = err instanceof Error && err.message.trim() ? err.message.trim() : "Sorry about that \u2014 I couldn't finish this right now. Happy to jump on a quick call if you'd like help \u2014 just let us know when works.";
|
|
759
|
+
if (showedInterim) {
|
|
760
|
+
appendAssistant(message);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
yield {
|
|
764
|
+
content: [{ type: "text", text: message }]
|
|
765
|
+
};
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (abortSignal.aborted) return;
|
|
769
|
+
if (data.superseded) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (data.provider) {
|
|
773
|
+
onProvider?.(data.provider);
|
|
774
|
+
}
|
|
775
|
+
const toolCalls = data.toolCalls ?? [];
|
|
776
|
+
let text = data.text ?? "";
|
|
777
|
+
if (toolCalls.length > 0) {
|
|
778
|
+
await onToolCalls?.(toolCalls);
|
|
779
|
+
turnRef.current.toolsApplied = true;
|
|
780
|
+
}
|
|
781
|
+
if (abortSignal.aborted) {
|
|
782
|
+
if (turnRef.current.toolsApplied) {
|
|
783
|
+
restoreState?.(turnRef.current.snapshot);
|
|
784
|
+
turnRef.current.toolsApplied = false;
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (toolCalls.length > 0 && !text.trim()) {
|
|
789
|
+
text = "Done \u2014 please check the updated UI.";
|
|
790
|
+
}
|
|
791
|
+
if (showedInterim) {
|
|
792
|
+
if (text.trim()) {
|
|
793
|
+
appendAssistant(text);
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
yield {
|
|
798
|
+
content: [{ type: "text", text }]
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}),
|
|
802
|
+
[]
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
var markdownComponents = (0, import_react_markdown.unstable_memoizeMarkdownComponents)({
|
|
806
|
+
h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "mb-2 mt-3 text-base font-semibold first:mt-0", children }),
|
|
807
|
+
h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "mb-2 mt-3 text-sm font-semibold first:mt-0", children }),
|
|
808
|
+
h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mb-1.5 mt-2 text-sm font-semibold first:mt-0", children }),
|
|
809
|
+
p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mb-2 last:mb-0 whitespace-pre-wrap", children }),
|
|
810
|
+
strong: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-semibold", children }),
|
|
811
|
+
em: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { className: "italic", children }),
|
|
812
|
+
ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "mb-2 list-disc space-y-1 pl-5 last:mb-0", children }),
|
|
813
|
+
ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "mb-2 list-decimal space-y-1 pl-5 last:mb-0", children }),
|
|
814
|
+
li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "leading-relaxed", children }),
|
|
815
|
+
a: ({ href, children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
816
|
+
"a",
|
|
817
|
+
{
|
|
818
|
+
href,
|
|
819
|
+
target: "_blank",
|
|
820
|
+
rel: "noreferrer",
|
|
821
|
+
className: "text-indigo-600 underline underline-offset-2 hover:text-indigo-700",
|
|
822
|
+
children
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { className: "mb-2 border-l-2 border-gray-300 pl-3 text-gray-600 italic last:mb-0", children }),
|
|
826
|
+
code: ({ className, children, ...props }) => {
|
|
827
|
+
const isBlock = Boolean(className?.includes("language-"));
|
|
828
|
+
if (isBlock) {
|
|
829
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: `${className ?? ""} text-xs`, ...props, children });
|
|
830
|
+
}
|
|
831
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
832
|
+
"code",
|
|
833
|
+
{
|
|
834
|
+
className: "rounded bg-gray-100 px-1 py-0.5 font-mono text-[0.85em] text-gray-800",
|
|
835
|
+
...props,
|
|
836
|
+
children
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
},
|
|
840
|
+
pre: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "mb-2 overflow-x-auto rounded-lg bg-gray-900 p-3 text-xs text-gray-100 last:mb-0", children }),
|
|
841
|
+
hr: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "my-3 border-gray-200" }),
|
|
842
|
+
table: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-2 overflow-x-auto last:mb-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("table", { className: "w-full border-collapse text-left text-xs", children }) }),
|
|
843
|
+
th: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "border-b border-gray-200 px-2 py-1 font-semibold", children }),
|
|
844
|
+
td: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "border-b border-gray-100 px-2 py-1 align-top", children })
|
|
845
|
+
});
|
|
846
|
+
function MarkdownText() {
|
|
847
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
848
|
+
import_react_markdown.MarkdownTextPrimitive,
|
|
849
|
+
{
|
|
850
|
+
remarkPlugins: [import_remark_gfm.default],
|
|
851
|
+
className: "aui-md",
|
|
852
|
+
smooth: false,
|
|
853
|
+
components: markdownComponents
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
function UserMessage() {
|
|
858
|
+
const message = (0, import_react3.useMessage)();
|
|
859
|
+
const imagesApi = (0, import_react2.useContext)(MessageImagesContext);
|
|
860
|
+
const sendersApi = (0, import_react2.useContext)(MessageSendersContext);
|
|
861
|
+
const images = imagesApi?.imagesByMessageId[message.id] ?? [];
|
|
862
|
+
const custom = message.metadata?.custom;
|
|
863
|
+
const sender = sendersApi?.senderByMessageId[message.id] ?? (custom?.senderName ? {
|
|
864
|
+
name: custom.senderName,
|
|
865
|
+
type: custom.senderType || "client"
|
|
866
|
+
} : null);
|
|
867
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.MessagePrimitive.Root, { className: "flex justify-end px-4 py-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-[80%]", children: [
|
|
868
|
+
sender && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mb-1 text-right text-[11px] font-medium text-gray-400", children: sender.name }),
|
|
869
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-2xl rounded-br-sm bg-indigo-600 px-4 py-2.5 text-sm text-white", children: [
|
|
870
|
+
images.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-2 flex flex-wrap gap-2", children: images.map((src, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
871
|
+
"a",
|
|
872
|
+
{
|
|
873
|
+
href: src,
|
|
874
|
+
target: "_blank",
|
|
875
|
+
rel: "noreferrer",
|
|
876
|
+
className: "block overflow-hidden rounded-lg border border-white/30 bg-white/10",
|
|
877
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
878
|
+
"img",
|
|
879
|
+
{
|
|
880
|
+
src,
|
|
881
|
+
alt: `Attached screenshot ${index + 1}`,
|
|
882
|
+
className: "max-h-40 max-w-full object-contain"
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
},
|
|
886
|
+
`${message.id}-${index}`
|
|
887
|
+
)) }),
|
|
888
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "whitespace-pre-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MentionText, { text: getMessageText(message), tone: "user" }) })
|
|
889
|
+
] })
|
|
890
|
+
] }) });
|
|
891
|
+
}
|
|
892
|
+
function AssistantMessage() {
|
|
893
|
+
const message = (0, import_react3.useMessage)();
|
|
894
|
+
const sendersApi = (0, import_react2.useContext)(MessageSendersContext);
|
|
895
|
+
const aiUser = (0, import_react2.useContext)(AiUserContext);
|
|
896
|
+
const custom = message.metadata?.custom;
|
|
897
|
+
const registered = sendersApi?.senderByMessageId[message.id];
|
|
898
|
+
const isDeveloper = custom?.senderType === "developer" || custom?.senderType == null && registered?.type === "developer";
|
|
899
|
+
const sender = isDeveloper ? {
|
|
900
|
+
name: custom?.senderName || registered?.name || "Developer",
|
|
901
|
+
type: "developer"
|
|
902
|
+
} : aiUser ? {
|
|
903
|
+
type: "ai",
|
|
904
|
+
name: aiUser.name !== "ai" ? aiUser.name : custom?.senderName && custom.senderName !== "ai" ? custom.senderName : registered?.type === "ai" && registered.name !== "ai" ? registered.name : aiUser.name
|
|
905
|
+
} : registered?.type === "ai" ? registered : null;
|
|
906
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.MessagePrimitive.Root, { className: "flex justify-start px-4 py-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-[85%]", children: [
|
|
907
|
+
sender && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
908
|
+
"p",
|
|
909
|
+
{
|
|
910
|
+
className: `mb-1 text-[11px] font-medium ${isDeveloper ? "text-emerald-700" : "text-gray-400"}`,
|
|
911
|
+
children: sender.name
|
|
912
|
+
}
|
|
913
|
+
),
|
|
914
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
915
|
+
"div",
|
|
916
|
+
{
|
|
917
|
+
className: `rounded-2xl rounded-bl-sm px-4 py-2.5 text-sm shadow-sm ${isDeveloper ? "border border-emerald-200 bg-emerald-50 text-emerald-950" : "border border-gray-200 bg-white text-gray-800"}`,
|
|
918
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.MessagePrimitive.Content, { components: { Text: MarkdownText } })
|
|
919
|
+
}
|
|
920
|
+
)
|
|
921
|
+
] }) });
|
|
922
|
+
}
|
|
923
|
+
function MessageRenderer() {
|
|
924
|
+
const message = (0, import_react3.useMessage)();
|
|
925
|
+
if (message.role === "assistant" && message.status?.type === "incomplete") {
|
|
926
|
+
if (!getMessageText(message).trim()) return null;
|
|
927
|
+
}
|
|
928
|
+
if (message.role === "user") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UserMessage, {});
|
|
929
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AssistantMessage, {});
|
|
930
|
+
}
|
|
931
|
+
function ComposerSendButton() {
|
|
932
|
+
const isEmpty = (0, import_react3.useComposer)((c) => c.isEmpty);
|
|
933
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
934
|
+
"button",
|
|
935
|
+
{
|
|
936
|
+
type: "submit",
|
|
937
|
+
disabled: isEmpty,
|
|
938
|
+
className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
|
|
939
|
+
"aria-label": "Send",
|
|
940
|
+
title: "Send",
|
|
941
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
942
|
+
"svg",
|
|
943
|
+
{
|
|
944
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
945
|
+
viewBox: "0 0 24 24",
|
|
946
|
+
fill: "none",
|
|
947
|
+
stroke: "currentColor",
|
|
948
|
+
strokeWidth: "2",
|
|
949
|
+
className: "h-5 w-5",
|
|
950
|
+
"aria-hidden": true,
|
|
951
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
952
|
+
"path",
|
|
953
|
+
{
|
|
954
|
+
d: "M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z",
|
|
955
|
+
strokeLinecap: "round",
|
|
956
|
+
strokeLinejoin: "round"
|
|
957
|
+
}
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
function chatUserStorageKey(conversationId) {
|
|
965
|
+
return `ai-ui:chat-auth:${conversationId}`;
|
|
966
|
+
}
|
|
967
|
+
function readStoredUserKey(conversationId, composableUsers) {
|
|
968
|
+
if (typeof window === "undefined" || composableUsers.length === 0) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const stored = sessionStorage.getItem(chatUserStorageKey(conversationId));
|
|
973
|
+
if (stored && composableUsers.some((u, i) => userKey(u, i) === stored)) {
|
|
974
|
+
return stored;
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
function writeStoredUserKey(conversationId, key) {
|
|
981
|
+
if (typeof window === "undefined") return;
|
|
982
|
+
try {
|
|
983
|
+
const storageKey = chatUserStorageKey(conversationId);
|
|
984
|
+
if (key) sessionStorage.setItem(storageKey, key);
|
|
985
|
+
else sessionStorage.removeItem(storageKey);
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function ChatLoginScreen({
|
|
990
|
+
users,
|
|
991
|
+
title,
|
|
992
|
+
className,
|
|
993
|
+
requirePassword,
|
|
994
|
+
onSelect
|
|
995
|
+
}) {
|
|
996
|
+
const [selectedKey, setSelectedKey] = (0, import_react2.useState)(null);
|
|
997
|
+
const [password, setPassword] = (0, import_react2.useState)("");
|
|
998
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
999
|
+
const [submitting, setSubmitting] = (0, import_react2.useState)(false);
|
|
1000
|
+
const entries = users.filter((user) => user.type === "client" || user.type === "developer").map((user, index) => ({
|
|
1001
|
+
user,
|
|
1002
|
+
key: userKey(user, index)
|
|
1003
|
+
}));
|
|
1004
|
+
const selected = entries.find((e) => e.key === selectedKey) ?? null;
|
|
1005
|
+
const handleSubmit = async (event) => {
|
|
1006
|
+
event.preventDefault();
|
|
1007
|
+
if (!selected) {
|
|
1008
|
+
setError("Select a user first");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (requirePassword && !password) {
|
|
1012
|
+
setError("Enter your password");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
setSubmitting(true);
|
|
1016
|
+
setError(null);
|
|
1017
|
+
try {
|
|
1018
|
+
await onSelect(selected.key, password);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
setError(
|
|
1021
|
+
err instanceof Error ? err.message : "Sign in failed. Try again."
|
|
1022
|
+
);
|
|
1023
|
+
} finally {
|
|
1024
|
+
setSubmitting(false);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `flex h-full flex-col bg-gray-50 ${className ?? ""}`, children: [
|
|
1028
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "border-b border-gray-200 bg-white px-5 py-4", children: [
|
|
1029
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "font-semibold text-gray-800", children: title }),
|
|
1030
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-gray-400", children: requirePassword ? "Sign in with your Maintainer Pro account" : "Choose who you are to continue" })
|
|
1031
|
+
] }),
|
|
1032
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 flex-col items-center justify-center overflow-y-auto p-6", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "w-full max-w-sm space-y-6", children: [
|
|
1033
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center", children: [
|
|
1034
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-base font-semibold text-gray-800", children: "Sign in" }),
|
|
1035
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1 text-sm text-gray-500", children: requirePassword ? "Select your account and enter your password. Everyone listed can still be @mentioned." : "Pick your identity for this chat. Everyone below can still be @mentioned." })
|
|
1036
|
+
] }),
|
|
1037
|
+
entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-center text-sm text-gray-500", children: "No users are available for this application yet." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "space-y-2", children: entries.map(({ user, key }) => {
|
|
1038
|
+
const active = key === selectedKey;
|
|
1039
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1040
|
+
"button",
|
|
1041
|
+
{
|
|
1042
|
+
type: "button",
|
|
1043
|
+
onClick: () => {
|
|
1044
|
+
setSelectedKey(key);
|
|
1045
|
+
setPassword("");
|
|
1046
|
+
setError(null);
|
|
1047
|
+
if (!requirePassword) {
|
|
1048
|
+
void onSelect(key);
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
className: `flex w-full items-center justify-between gap-3 rounded-xl border px-4 py-3 text-left shadow-sm transition-colors ${active ? "border-teal-400 bg-teal-50/50" : "border-gray-200 bg-white hover:border-teal-300 hover:bg-teal-50/40"}`,
|
|
1052
|
+
children: [
|
|
1053
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "min-w-0", children: [
|
|
1054
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "block font-medium text-gray-800", children: user.name }),
|
|
1055
|
+
user.email && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "block truncate text-xs text-gray-400", children: user.email })
|
|
1056
|
+
] }),
|
|
1057
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "shrink-0 rounded-md bg-gray-100 px-2 py-0.5 text-[11px] font-medium capitalize text-gray-600", children: userBadge(user) })
|
|
1058
|
+
]
|
|
1059
|
+
}
|
|
1060
|
+
) }, key);
|
|
1061
|
+
}) }),
|
|
1062
|
+
requirePassword && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: (e) => void handleSubmit(e), className: "space-y-3", children: [
|
|
1063
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
1064
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1065
|
+
"label",
|
|
1066
|
+
{
|
|
1067
|
+
htmlFor: "ai-chat-password",
|
|
1068
|
+
className: "mb-1 block text-xs font-medium text-gray-600",
|
|
1069
|
+
children: [
|
|
1070
|
+
"Password",
|
|
1071
|
+
selected?.user.email ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-normal text-gray-400", children: [
|
|
1072
|
+
" ",
|
|
1073
|
+
"for ",
|
|
1074
|
+
selected.user.email
|
|
1075
|
+
] }) : null
|
|
1076
|
+
]
|
|
1077
|
+
}
|
|
1078
|
+
),
|
|
1079
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1080
|
+
"input",
|
|
1081
|
+
{
|
|
1082
|
+
id: "ai-chat-password",
|
|
1083
|
+
type: "password",
|
|
1084
|
+
autoComplete: "current-password",
|
|
1085
|
+
value: password,
|
|
1086
|
+
onChange: (e) => {
|
|
1087
|
+
setPassword(e.target.value);
|
|
1088
|
+
setError(null);
|
|
1089
|
+
},
|
|
1090
|
+
disabled: !selected || submitting,
|
|
1091
|
+
className: "w-full rounded-xl border border-gray-200 bg-white px-3 py-2.5 text-sm text-gray-800 outline-none ring-teal-500/30 placeholder:text-gray-400 focus:border-teal-400 focus:ring-2 disabled:bg-gray-50 disabled:text-gray-400",
|
|
1092
|
+
placeholder: selected ? "Enter password" : "Select a user first"
|
|
1093
|
+
}
|
|
1094
|
+
)
|
|
1095
|
+
] }),
|
|
1096
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-red-600", role: "alert", children: error }),
|
|
1097
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1098
|
+
"button",
|
|
1099
|
+
{
|
|
1100
|
+
type: "submit",
|
|
1101
|
+
disabled: !selected || !password || submitting,
|
|
1102
|
+
className: "w-full rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-40",
|
|
1103
|
+
children: submitting ? "Signing in\u2026" : "Sign in"
|
|
1104
|
+
}
|
|
1105
|
+
)
|
|
1106
|
+
] })
|
|
1107
|
+
] }) })
|
|
1108
|
+
] });
|
|
1109
|
+
}
|
|
1110
|
+
function ComposerMentionInput({
|
|
1111
|
+
users,
|
|
1112
|
+
excludeUser,
|
|
1113
|
+
isDeveloper,
|
|
1114
|
+
onPaste,
|
|
1115
|
+
onSubmitKeyDown
|
|
1116
|
+
}) {
|
|
1117
|
+
const composerRuntime = (0, import_react3.useComposerRuntime)();
|
|
1118
|
+
const wrapRef = (0, import_react2.useRef)(null);
|
|
1119
|
+
const [mention, setMention] = (0, import_react2.useState)(null);
|
|
1120
|
+
const [activeIndex, setActiveIndex] = (0, import_react2.useState)(0);
|
|
1121
|
+
const filtered = (0, import_react2.useMemo)(() => {
|
|
1122
|
+
if (!mention || users.length === 0) return [];
|
|
1123
|
+
const q = mention.query.toLowerCase();
|
|
1124
|
+
return users.filter((u) => {
|
|
1125
|
+
if (excludeUser && u.type === excludeUser.type && u.name.toLowerCase() === excludeUser.name.toLowerCase()) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
return u.name.toLowerCase().includes(q) || u.type.toLowerCase().includes(q) || (u.role?.toLowerCase().includes(q) ?? false);
|
|
1129
|
+
});
|
|
1130
|
+
}, [mention, users, excludeUser]);
|
|
1131
|
+
(0, import_react2.useEffect)(() => {
|
|
1132
|
+
setActiveIndex(0);
|
|
1133
|
+
}, [mention?.start, mention?.query, filtered.length]);
|
|
1134
|
+
const syncMention = (textarea) => {
|
|
1135
|
+
setMention(getActiveMention(textarea.value, textarea.selectionStart));
|
|
1136
|
+
};
|
|
1137
|
+
const insertMention = (user) => {
|
|
1138
|
+
if (!mention) return;
|
|
1139
|
+
const textarea = wrapRef.current?.querySelector("textarea");
|
|
1140
|
+
const text = textarea?.value ?? composerRuntime.getState().text;
|
|
1141
|
+
const cursor = textarea?.selectionStart ?? text.length;
|
|
1142
|
+
const before = text.slice(0, mention.start);
|
|
1143
|
+
const after = text.slice(cursor);
|
|
1144
|
+
const inserted = `@${user.name} `;
|
|
1145
|
+
const next = `${before}${inserted}${after}`;
|
|
1146
|
+
composerRuntime.setText(next);
|
|
1147
|
+
setMention(null);
|
|
1148
|
+
requestAnimationFrame(() => {
|
|
1149
|
+
const el = wrapRef.current?.querySelector("textarea");
|
|
1150
|
+
if (!el) return;
|
|
1151
|
+
const pos = before.length + inserted.length;
|
|
1152
|
+
el.focus();
|
|
1153
|
+
el.setSelectionRange(pos, pos);
|
|
1154
|
+
});
|
|
1155
|
+
};
|
|
1156
|
+
const handleKeyDown = (event) => {
|
|
1157
|
+
if (mention && filtered.length > 0) {
|
|
1158
|
+
if (event.key === "ArrowDown") {
|
|
1159
|
+
event.preventDefault();
|
|
1160
|
+
setActiveIndex((i) => (i + 1) % filtered.length);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (event.key === "ArrowUp") {
|
|
1164
|
+
event.preventDefault();
|
|
1165
|
+
setActiveIndex((i) => (i - 1 + filtered.length) % filtered.length);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
1169
|
+
event.preventDefault();
|
|
1170
|
+
insertMention(filtered[activeIndex] ?? filtered[0]);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (event.key === "Escape") {
|
|
1174
|
+
event.preventDefault();
|
|
1175
|
+
setMention(null);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
onSubmitKeyDown(event);
|
|
1180
|
+
};
|
|
1181
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: wrapRef, className: "relative flex min-w-0 flex-1 items-end", children: [
|
|
1182
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1183
|
+
import_react3.ComposerPrimitive.Input,
|
|
1184
|
+
{
|
|
1185
|
+
placeholder: isDeveloper ? "Reply without running AI\u2026 (@ to tag)" : users.length > 0 ? "Message\u2026 (@ to tag someone)" : "Message\u2026 (paste a screenshot anytime)",
|
|
1186
|
+
className: "box-border block min-h-10 max-h-32 w-full resize-none rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm leading-5 text-gray-800 placeholder-gray-400 focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-100",
|
|
1187
|
+
rows: 1,
|
|
1188
|
+
onPaste,
|
|
1189
|
+
onKeyDown: handleKeyDown,
|
|
1190
|
+
onKeyUp: (event) => syncMention(event.currentTarget),
|
|
1191
|
+
onClick: (event) => syncMention(event.currentTarget)
|
|
1192
|
+
}
|
|
1193
|
+
),
|
|
1194
|
+
mention && filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1195
|
+
"div",
|
|
1196
|
+
{
|
|
1197
|
+
role: "listbox",
|
|
1198
|
+
"aria-label": "Tag user",
|
|
1199
|
+
className: "absolute bottom-full left-0 z-20 mb-1.5 max-h-48 min-w-[12rem] overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg",
|
|
1200
|
+
children: [
|
|
1201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "px-3 py-1 text-[10px] font-medium uppercase tracking-wide text-gray-400", children: "Tag someone" }),
|
|
1202
|
+
filtered.map((user, index) => {
|
|
1203
|
+
const active = index === activeIndex;
|
|
1204
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1205
|
+
"button",
|
|
1206
|
+
{
|
|
1207
|
+
type: "button",
|
|
1208
|
+
role: "option",
|
|
1209
|
+
"aria-selected": active,
|
|
1210
|
+
onMouseDown: (event) => {
|
|
1211
|
+
event.preventDefault();
|
|
1212
|
+
insertMention(user);
|
|
1213
|
+
},
|
|
1214
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
1215
|
+
className: `flex w-full items-center justify-between gap-3 px-3 py-1.5 text-left text-xs transition-colors ${active ? "bg-indigo-50 text-indigo-700" : "text-gray-700 hover:bg-gray-50"}`,
|
|
1216
|
+
children: [
|
|
1217
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-medium", children: [
|
|
1218
|
+
"@",
|
|
1219
|
+
user.name
|
|
1220
|
+
] }),
|
|
1221
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1222
|
+
"span",
|
|
1223
|
+
{
|
|
1224
|
+
className: `text-[10px] capitalize ${active ? "text-indigo-500" : "text-gray-400"}`,
|
|
1225
|
+
children: userBadge(user)
|
|
1226
|
+
}
|
|
1227
|
+
)
|
|
1228
|
+
]
|
|
1229
|
+
},
|
|
1230
|
+
`${user.type}:${user.name}:${index}`
|
|
1231
|
+
);
|
|
1232
|
+
})
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
)
|
|
1236
|
+
] });
|
|
1237
|
+
}
|
|
1238
|
+
function useInterruptibleSubmit(options) {
|
|
1239
|
+
const threadRuntime = (0, import_react3.useThreadRuntime)();
|
|
1240
|
+
const composerRuntime = (0, import_react3.useComposerRuntime)();
|
|
1241
|
+
const optsRef = (0, import_react2.useRef)(options);
|
|
1242
|
+
optsRef.current = options;
|
|
1243
|
+
return (0, import_react2.useCallback)(() => {
|
|
1244
|
+
const composer = composerRuntime.getState();
|
|
1245
|
+
if (composer.isEmpty) return;
|
|
1246
|
+
const {
|
|
1247
|
+
restoreState,
|
|
1248
|
+
turnRef,
|
|
1249
|
+
getSelectedUser,
|
|
1250
|
+
apiUrl,
|
|
1251
|
+
conversationId,
|
|
1252
|
+
registerSender
|
|
1253
|
+
} = optsRef.current;
|
|
1254
|
+
const selectedUser = getSelectedUser();
|
|
1255
|
+
if (selectedUser?.type === "developer") {
|
|
1256
|
+
const text = composer.text.trim();
|
|
1257
|
+
if (!text) return;
|
|
1258
|
+
void composerRuntime.reset();
|
|
1259
|
+
const beforeIds = new Set(
|
|
1260
|
+
threadRuntime.getState().messages.map((m) => m.id)
|
|
1261
|
+
);
|
|
1262
|
+
threadRuntime.append({
|
|
1263
|
+
role: "assistant",
|
|
1264
|
+
content: [{ type: "text", text }],
|
|
1265
|
+
metadata: {
|
|
1266
|
+
custom: {
|
|
1267
|
+
senderType: "developer",
|
|
1268
|
+
senderName: selectedUser.name
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
startRun: false
|
|
1272
|
+
});
|
|
1273
|
+
const added = [...threadRuntime.getState().messages].reverse().find((m) => !beforeIds.has(m.id));
|
|
1274
|
+
if (added) {
|
|
1275
|
+
registerSender(added.id, {
|
|
1276
|
+
name: selectedUser.name,
|
|
1277
|
+
type: "developer"
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
persistDeveloperReply(apiUrl, conversationId, text, selectedUser.name);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const thread = threadRuntime.getState();
|
|
1284
|
+
if (!thread.isRunning) {
|
|
1285
|
+
composerRuntime.send();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
threadRuntime.cancelRun();
|
|
1289
|
+
if (turnRef.current.toolsApplied || turnRef.current.snapshot !== void 0) {
|
|
1290
|
+
restoreState?.(turnRef.current.snapshot);
|
|
1291
|
+
}
|
|
1292
|
+
turnRef.current = { snapshot: void 0, toolsApplied: false };
|
|
1293
|
+
composerRuntime.send();
|
|
1294
|
+
}, [composerRuntime, threadRuntime]);
|
|
1295
|
+
}
|
|
1296
|
+
function ChatUI({
|
|
1297
|
+
apiUrl,
|
|
1298
|
+
title,
|
|
1299
|
+
emptyState,
|
|
1300
|
+
className,
|
|
1301
|
+
pendingImages,
|
|
1302
|
+
onAddImages,
|
|
1303
|
+
onRemoveImage,
|
|
1304
|
+
restoreState,
|
|
1305
|
+
turnRef,
|
|
1306
|
+
users,
|
|
1307
|
+
onSwitchUser,
|
|
1308
|
+
conversationId,
|
|
1309
|
+
registerSender,
|
|
1310
|
+
getSelectedUser,
|
|
1311
|
+
onProvider
|
|
1312
|
+
}) {
|
|
1313
|
+
const [providerLabel, setProviderLabel] = (0, import_react2.useState)("AI CLI");
|
|
1314
|
+
const fileInputRef = (0, import_react2.useRef)(null);
|
|
1315
|
+
const submit = useInterruptibleSubmit({
|
|
1316
|
+
restoreState,
|
|
1317
|
+
turnRef,
|
|
1318
|
+
getSelectedUser,
|
|
1319
|
+
apiUrl,
|
|
1320
|
+
conversationId,
|
|
1321
|
+
registerSender
|
|
1322
|
+
});
|
|
1323
|
+
const selectedUser = getSelectedUser();
|
|
1324
|
+
const isDeveloper = selectedUser?.type === "developer";
|
|
1325
|
+
(0, import_react2.useEffect)(() => {
|
|
1326
|
+
let cancelled = false;
|
|
1327
|
+
fetch(apiUrl).then(async (res) => {
|
|
1328
|
+
const data = await res.json();
|
|
1329
|
+
if (!cancelled) {
|
|
1330
|
+
setProviderLabel(
|
|
1331
|
+
res.ok && data.providerLabel ? data.providerLabel : "No AI CLI found"
|
|
1332
|
+
);
|
|
1333
|
+
if (res.ok && data.provider) {
|
|
1334
|
+
onProvider?.(data.provider);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}).catch(() => {
|
|
1338
|
+
if (!cancelled) setProviderLabel("No AI CLI found");
|
|
1339
|
+
});
|
|
1340
|
+
return () => {
|
|
1341
|
+
cancelled = true;
|
|
1342
|
+
};
|
|
1343
|
+
}, [apiUrl, onProvider]);
|
|
1344
|
+
const handlePaste = (event) => {
|
|
1345
|
+
const items = event.clipboardData?.items;
|
|
1346
|
+
if (!items) return;
|
|
1347
|
+
const files = [];
|
|
1348
|
+
for (const item of Array.from(items)) {
|
|
1349
|
+
if (item.type.startsWith("image/")) {
|
|
1350
|
+
const file = item.getAsFile();
|
|
1351
|
+
if (file) files.push(file);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (files.length > 0) {
|
|
1355
|
+
event.preventDefault();
|
|
1356
|
+
onAddImages(files);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const handleFileChange = (event) => {
|
|
1360
|
+
const files = Array.from(event.target.files ?? []);
|
|
1361
|
+
if (files.length > 0) onAddImages(files);
|
|
1362
|
+
event.target.value = "";
|
|
1363
|
+
};
|
|
1364
|
+
const handleSubmit = (event) => {
|
|
1365
|
+
event.preventDefault();
|
|
1366
|
+
void submit();
|
|
1367
|
+
};
|
|
1368
|
+
const handleInputKeyDown = (event) => {
|
|
1369
|
+
if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
event.preventDefault();
|
|
1373
|
+
void submit();
|
|
1374
|
+
};
|
|
1375
|
+
const mentionUsers = users ?? [];
|
|
1376
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `flex h-full flex-col bg-gray-50 ${className ?? ""}`, children: [
|
|
1377
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-3 border-b border-gray-200 bg-white px-5 py-4 pr-12", children: [
|
|
1378
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
|
|
1379
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "font-semibold text-gray-800", children: title }),
|
|
1380
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-xs text-gray-400", children: [
|
|
1381
|
+
"Powered by ",
|
|
1382
|
+
providerLabel
|
|
1383
|
+
] })
|
|
1384
|
+
] }),
|
|
1385
|
+
selectedUser && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1386
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1387
|
+
"span",
|
|
1388
|
+
{
|
|
1389
|
+
className: `rounded-full px-2.5 py-0.5 text-xs font-medium ${isDeveloper ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-600"}`,
|
|
1390
|
+
children: selectedUser.name
|
|
1391
|
+
}
|
|
1392
|
+
),
|
|
1393
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1394
|
+
"button",
|
|
1395
|
+
{
|
|
1396
|
+
type: "button",
|
|
1397
|
+
onClick: onSwitchUser,
|
|
1398
|
+
className: "text-[11px] text-gray-400 underline-offset-2 hover:text-gray-600 hover:underline",
|
|
1399
|
+
children: "Switch user"
|
|
1400
|
+
}
|
|
1401
|
+
)
|
|
1402
|
+
] })
|
|
1403
|
+
] }),
|
|
1404
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react3.ThreadPrimitive.Root, { className: "flex flex-1 flex-col overflow-hidden", children: [
|
|
1405
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react3.ThreadPrimitive.Viewport, { className: "flex-1 overflow-y-auto py-4 space-y-1", children: [
|
|
1406
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.ThreadPrimitive.Empty, { children: emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex h-full items-center justify-center p-8 text-center text-sm text-gray-400", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-medium text-gray-500", children: "Ask me to help with this app" }) }) }),
|
|
1407
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.ThreadPrimitive.Messages, { components: { Message: MessageRenderer } })
|
|
1408
|
+
] }),
|
|
1409
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react3.ThreadPrimitive.ViewportFooter, { className: "bg-gray-50", children: [
|
|
1410
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-end px-4 pb-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.ThreadPrimitive.ScrollToBottom, { className: "rounded-full bg-white border border-gray-200 px-3 py-1 text-xs text-gray-500 shadow hover:bg-gray-50 transition-colors", children: "Scroll to bottom" }) }),
|
|
1411
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1412
|
+
import_react3.ComposerPrimitive.Root,
|
|
1413
|
+
{
|
|
1414
|
+
className: "border-t border-gray-200 bg-white p-4",
|
|
1415
|
+
onSubmit: handleSubmit,
|
|
1416
|
+
children: [
|
|
1417
|
+
pendingImages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-3 flex flex-wrap gap-2", children: pendingImages.map((img) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1418
|
+
"div",
|
|
1419
|
+
{
|
|
1420
|
+
className: "group relative h-16 w-16 overflow-hidden rounded-lg border border-gray-200 bg-gray-50",
|
|
1421
|
+
children: [
|
|
1422
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1423
|
+
"img",
|
|
1424
|
+
{
|
|
1425
|
+
src: img.dataUrl,
|
|
1426
|
+
alt: img.name,
|
|
1427
|
+
className: "h-full w-full object-cover"
|
|
1428
|
+
}
|
|
1429
|
+
),
|
|
1430
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1431
|
+
"button",
|
|
1432
|
+
{
|
|
1433
|
+
type: "button",
|
|
1434
|
+
onClick: () => onRemoveImage(img.id),
|
|
1435
|
+
className: "absolute right-0.5 top-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100",
|
|
1436
|
+
"aria-label": "Remove screenshot",
|
|
1437
|
+
children: "\xD7"
|
|
1438
|
+
}
|
|
1439
|
+
)
|
|
1440
|
+
]
|
|
1441
|
+
},
|
|
1442
|
+
img.id
|
|
1443
|
+
)) }),
|
|
1444
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-end gap-2", children: [
|
|
1445
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1446
|
+
"input",
|
|
1447
|
+
{
|
|
1448
|
+
ref: fileInputRef,
|
|
1449
|
+
type: "file",
|
|
1450
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1451
|
+
multiple: true,
|
|
1452
|
+
className: "hidden",
|
|
1453
|
+
onChange: handleFileChange
|
|
1454
|
+
}
|
|
1455
|
+
),
|
|
1456
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1457
|
+
"button",
|
|
1458
|
+
{
|
|
1459
|
+
type: "button",
|
|
1460
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1461
|
+
className: "flex h-10 w-10 shrink-0 items-center justify-center self-end rounded-lg border border-gray-300 text-gray-500 hover:bg-gray-50 hover:text-gray-700 transition-colors",
|
|
1462
|
+
"aria-label": "Attach screenshot",
|
|
1463
|
+
title: "Attach screenshot",
|
|
1464
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1465
|
+
"svg",
|
|
1466
|
+
{
|
|
1467
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1468
|
+
viewBox: "0 0 24 24",
|
|
1469
|
+
fill: "none",
|
|
1470
|
+
stroke: "currentColor",
|
|
1471
|
+
strokeWidth: "2",
|
|
1472
|
+
className: "h-5 w-5",
|
|
1473
|
+
"aria-hidden": true,
|
|
1474
|
+
children: [
|
|
1475
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8.5", cy: "10.5", r: "1.5" }),
|
|
1477
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m21 15-4.5-4.5L9 18", strokeLinecap: "round" })
|
|
1478
|
+
]
|
|
1479
|
+
}
|
|
1480
|
+
)
|
|
1481
|
+
}
|
|
1482
|
+
),
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1484
|
+
ComposerMentionInput,
|
|
1485
|
+
{
|
|
1486
|
+
users: mentionUsers,
|
|
1487
|
+
excludeUser: selectedUser,
|
|
1488
|
+
isDeveloper,
|
|
1489
|
+
onPaste: handlePaste,
|
|
1490
|
+
onSubmitKeyDown: handleInputKeyDown
|
|
1491
|
+
}
|
|
1492
|
+
),
|
|
1493
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComposerSendButton, {})
|
|
1494
|
+
] }),
|
|
1495
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-2 text-[11px] text-gray-400", children: isDeveloper ? "Your reply is saved to chat history only \u2014 AI will not run." : `Paste or attach screenshots (PNG/JPG, up to ${MAX_IMAGES}). Type @ to tag someone.` })
|
|
1496
|
+
]
|
|
1497
|
+
}
|
|
1498
|
+
)
|
|
1499
|
+
] })
|
|
1500
|
+
] })
|
|
1501
|
+
] });
|
|
1502
|
+
}
|
|
1503
|
+
function AiChat({
|
|
1504
|
+
apiUrl = "/api/chat",
|
|
1505
|
+
buildContext,
|
|
1506
|
+
onToolCalls,
|
|
1507
|
+
captureState,
|
|
1508
|
+
restoreState,
|
|
1509
|
+
conversationId: conversationIdProp,
|
|
1510
|
+
title = "AI Assistant",
|
|
1511
|
+
emptyState,
|
|
1512
|
+
className,
|
|
1513
|
+
users,
|
|
1514
|
+
maintainerProUrl,
|
|
1515
|
+
maintainerProApiKey
|
|
1516
|
+
}) {
|
|
1517
|
+
const resolvedBuildContext = buildContext ?? (() => buildBrowserClientContext());
|
|
1518
|
+
const [boot, setBoot] = (0, import_react2.useState)(null);
|
|
1519
|
+
(0, import_react2.useEffect)(() => {
|
|
1520
|
+
let cancelled = false;
|
|
1521
|
+
async function bootChat() {
|
|
1522
|
+
try {
|
|
1523
|
+
const providerRes = await fetch(apiUrl).catch(() => null);
|
|
1524
|
+
const providerData = providerRes ? await providerRes.json().catch(() => ({})) : {};
|
|
1525
|
+
let conversationId = conversationIdProp ?? providerData.conversationId ?? "shared";
|
|
1526
|
+
let storedMessages = providerData.messages ?? [];
|
|
1527
|
+
let resolvedUsers = users ?? [];
|
|
1528
|
+
if (maintainerProUrl && maintainerProApiKey) {
|
|
1529
|
+
const mp = createMaintainerProClient({
|
|
1530
|
+
baseUrl: maintainerProUrl,
|
|
1531
|
+
apiKey: maintainerProApiKey
|
|
1532
|
+
});
|
|
1533
|
+
if (!conversationIdProp) {
|
|
1534
|
+
const conversations = await mp.listConversations();
|
|
1535
|
+
conversationId = conversations[0]?.id ?? "shared";
|
|
1536
|
+
}
|
|
1537
|
+
if (conversationId && conversationId !== "shared") {
|
|
1538
|
+
rememberConversationId(
|
|
1539
|
+
maintainerProUrl,
|
|
1540
|
+
maintainerProApiKey,
|
|
1541
|
+
conversationId
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
storedMessages = await mp.listMessages(conversationId);
|
|
1545
|
+
if (users === void 0) {
|
|
1546
|
+
try {
|
|
1547
|
+
resolvedUsers = await mp.listChatUsers();
|
|
1548
|
+
} catch {
|
|
1549
|
+
resolvedUsers = [];
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const providerId = providerData.provider ?? null;
|
|
1554
|
+
const aiFallback = resolveAiUser(resolvedUsers, providerId);
|
|
1555
|
+
const initialSenders = {};
|
|
1556
|
+
const initialMessages = storedMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m, index) => {
|
|
1557
|
+
const sender = senderFromStoredMessage(m, aiFallback);
|
|
1558
|
+
const id = m.id ?? `boot-${index}`;
|
|
1559
|
+
if (sender) {
|
|
1560
|
+
initialSenders[id] = sender;
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
id,
|
|
1564
|
+
role: m.role,
|
|
1565
|
+
content: [{ type: "text", text: m.content || "" }],
|
|
1566
|
+
...sender ? {
|
|
1567
|
+
metadata: {
|
|
1568
|
+
custom: {
|
|
1569
|
+
senderType: sender.type,
|
|
1570
|
+
senderName: sender.name
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
} : {}
|
|
1574
|
+
};
|
|
1575
|
+
});
|
|
1576
|
+
if (!cancelled) {
|
|
1577
|
+
setBoot({
|
|
1578
|
+
conversationId,
|
|
1579
|
+
initialMessages,
|
|
1580
|
+
initialSenders,
|
|
1581
|
+
providerId,
|
|
1582
|
+
users: resolvedUsers
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
} catch {
|
|
1586
|
+
if (!cancelled) {
|
|
1587
|
+
setBoot({
|
|
1588
|
+
conversationId: conversationIdProp ?? "shared",
|
|
1589
|
+
initialMessages: [],
|
|
1590
|
+
initialSenders: {},
|
|
1591
|
+
providerId: null,
|
|
1592
|
+
users: users ?? []
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
void bootChat();
|
|
1598
|
+
return () => {
|
|
1599
|
+
cancelled = true;
|
|
1600
|
+
};
|
|
1601
|
+
}, [
|
|
1602
|
+
apiUrl,
|
|
1603
|
+
conversationIdProp,
|
|
1604
|
+
maintainerProUrl,
|
|
1605
|
+
maintainerProApiKey,
|
|
1606
|
+
users
|
|
1607
|
+
]);
|
|
1608
|
+
if (!boot) {
|
|
1609
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1610
|
+
"div",
|
|
1611
|
+
{
|
|
1612
|
+
className: `flex h-full items-center justify-center bg-gray-50 text-sm text-gray-400 ${className ?? ""}`,
|
|
1613
|
+
children: "Loading chat\u2026"
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1618
|
+
AiChatReady,
|
|
1619
|
+
{
|
|
1620
|
+
apiUrl,
|
|
1621
|
+
buildContext: resolvedBuildContext,
|
|
1622
|
+
onToolCalls,
|
|
1623
|
+
captureState,
|
|
1624
|
+
restoreState,
|
|
1625
|
+
conversationId: boot.conversationId,
|
|
1626
|
+
initialMessages: boot.initialMessages,
|
|
1627
|
+
initialSenders: boot.initialSenders,
|
|
1628
|
+
providerId: boot.providerId,
|
|
1629
|
+
title,
|
|
1630
|
+
emptyState,
|
|
1631
|
+
className,
|
|
1632
|
+
users: boot.users,
|
|
1633
|
+
maintainerProUrl,
|
|
1634
|
+
maintainerProApiKey
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
function AiChatReady({
|
|
1639
|
+
apiUrl,
|
|
1640
|
+
buildContext,
|
|
1641
|
+
onToolCalls,
|
|
1642
|
+
captureState,
|
|
1643
|
+
restoreState,
|
|
1644
|
+
conversationId,
|
|
1645
|
+
initialMessages,
|
|
1646
|
+
initialSenders,
|
|
1647
|
+
providerId,
|
|
1648
|
+
title,
|
|
1649
|
+
emptyState,
|
|
1650
|
+
className,
|
|
1651
|
+
users,
|
|
1652
|
+
maintainerProUrl,
|
|
1653
|
+
maintainerProApiKey
|
|
1654
|
+
}) {
|
|
1655
|
+
const [pendingImages, setPendingImages] = (0, import_react2.useState)([]);
|
|
1656
|
+
const [imagesByMessageId, setImagesByMessageId] = (0, import_react2.useState)({});
|
|
1657
|
+
const [senderByMessageId, setSenderByMessageId] = (0, import_react2.useState)(initialSenders);
|
|
1658
|
+
const [activeProviderId, setActiveProviderId] = (0, import_react2.useState)(
|
|
1659
|
+
providerId
|
|
1660
|
+
);
|
|
1661
|
+
const aiUser = (0, import_react2.useMemo)(
|
|
1662
|
+
() => resolveAiUser(users, activeProviderId),
|
|
1663
|
+
[users, activeProviderId]
|
|
1664
|
+
);
|
|
1665
|
+
const onProvider = (0, import_react2.useCallback)((id) => {
|
|
1666
|
+
setActiveProviderId((prev) => prev === id ? prev : id);
|
|
1667
|
+
}, []);
|
|
1668
|
+
const composableUsers = (0, import_react2.useMemo)(
|
|
1669
|
+
() => (users ?? []).filter(isComposableUser),
|
|
1670
|
+
[users]
|
|
1671
|
+
);
|
|
1672
|
+
const requirePassword = Boolean(
|
|
1673
|
+
maintainerProUrl && maintainerProApiKey && composableUsers.some((u) => Boolean(u.email))
|
|
1674
|
+
);
|
|
1675
|
+
const [selectedUserKey, setSelectedUserKey] = (0, import_react2.useState)(
|
|
1676
|
+
() => readStoredUserKey(conversationId, (users ?? []).filter(isComposableUser))
|
|
1677
|
+
);
|
|
1678
|
+
const pendingRef = (0, import_react2.useRef)([]);
|
|
1679
|
+
pendingRef.current = pendingImages;
|
|
1680
|
+
const turnRef = (0, import_react2.useRef)({
|
|
1681
|
+
snapshot: void 0,
|
|
1682
|
+
toolsApplied: false
|
|
1683
|
+
});
|
|
1684
|
+
const appendAssistantRef = (0, import_react2.useRef)(() => {
|
|
1685
|
+
});
|
|
1686
|
+
const selectedUserRef = (0, import_react2.useRef)(null);
|
|
1687
|
+
const getSelectedUser = (0, import_react2.useCallback)(() => {
|
|
1688
|
+
if (composableUsers.length === 0 || !selectedUserKey) return null;
|
|
1689
|
+
const index = composableUsers.findIndex(
|
|
1690
|
+
(u, i) => userKey(u, i) === selectedUserKey
|
|
1691
|
+
);
|
|
1692
|
+
return index >= 0 ? composableUsers[index] : null;
|
|
1693
|
+
}, [composableUsers, selectedUserKey]);
|
|
1694
|
+
selectedUserRef.current = getSelectedUser();
|
|
1695
|
+
const handleSelectUser = (0, import_react2.useCallback)(
|
|
1696
|
+
async (key, password) => {
|
|
1697
|
+
const index = composableUsers.findIndex(
|
|
1698
|
+
(u, i) => userKey(u, i) === key
|
|
1699
|
+
);
|
|
1700
|
+
const user = index >= 0 ? composableUsers[index] : null;
|
|
1701
|
+
if (!user) throw new Error("Unknown user");
|
|
1702
|
+
if (requirePassword) {
|
|
1703
|
+
if (!user.email) throw new Error("This account has no email");
|
|
1704
|
+
if (!password) throw new Error("Enter your password");
|
|
1705
|
+
if (!maintainerProUrl || !maintainerProApiKey) {
|
|
1706
|
+
throw new Error("Maintainer Pro is not configured");
|
|
1707
|
+
}
|
|
1708
|
+
const mp = createMaintainerProClient({
|
|
1709
|
+
baseUrl: maintainerProUrl,
|
|
1710
|
+
apiKey: maintainerProApiKey
|
|
1711
|
+
});
|
|
1712
|
+
const authed = await mp.loginChatUser(user.email, password);
|
|
1713
|
+
if (authed.type !== user.type || authed.name.toLowerCase() !== user.name.toLowerCase()) {
|
|
1714
|
+
throw new Error("Signed-in user does not match selection");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
setSelectedUserKey(key);
|
|
1718
|
+
writeStoredUserKey(conversationId, key);
|
|
1719
|
+
},
|
|
1720
|
+
[
|
|
1721
|
+
composableUsers,
|
|
1722
|
+
conversationId,
|
|
1723
|
+
maintainerProApiKey,
|
|
1724
|
+
maintainerProUrl,
|
|
1725
|
+
requirePassword
|
|
1726
|
+
]
|
|
1727
|
+
);
|
|
1728
|
+
const handleSwitchUser = (0, import_react2.useCallback)(() => {
|
|
1729
|
+
setSelectedUserKey(null);
|
|
1730
|
+
writeStoredUserKey(conversationId, null);
|
|
1731
|
+
}, [conversationId]);
|
|
1732
|
+
const needsLogin = composableUsers.length > 0 && !selectedUserKey;
|
|
1733
|
+
const registerImages = (0, import_react2.useCallback)((messageId, dataUrls) => {
|
|
1734
|
+
setImagesByMessageId((prev) => ({ ...prev, [messageId]: dataUrls }));
|
|
1735
|
+
}, []);
|
|
1736
|
+
const registerSender = (0, import_react2.useCallback)(
|
|
1737
|
+
(messageId, sender) => {
|
|
1738
|
+
setSenderByMessageId((prev) => ({ ...prev, [messageId]: sender }));
|
|
1739
|
+
},
|
|
1740
|
+
[]
|
|
1741
|
+
);
|
|
1742
|
+
const takePending = (0, import_react2.useCallback)(() => {
|
|
1743
|
+
const current = pendingRef.current;
|
|
1744
|
+
pendingRef.current = [];
|
|
1745
|
+
setPendingImages([]);
|
|
1746
|
+
return current;
|
|
1747
|
+
}, []);
|
|
1748
|
+
const onAddImages = (0, import_react2.useCallback)(async (files) => {
|
|
1749
|
+
try {
|
|
1750
|
+
const next = [];
|
|
1751
|
+
for (const file of files) {
|
|
1752
|
+
next.push(await fileToPending(file));
|
|
1753
|
+
}
|
|
1754
|
+
setPendingImages((prev) => [...prev, ...next].slice(0, MAX_IMAGES));
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
console.error(err);
|
|
1757
|
+
window.alert(err instanceof Error ? err.message : "Could not add image");
|
|
1758
|
+
}
|
|
1759
|
+
}, []);
|
|
1760
|
+
const onRemoveImage = (0, import_react2.useCallback)((id) => {
|
|
1761
|
+
setPendingImages((prev) => prev.filter((img) => img.id !== id));
|
|
1762
|
+
}, []);
|
|
1763
|
+
const appendAssistant = (0, import_react2.useCallback)((text) => {
|
|
1764
|
+
appendAssistantRef.current(text);
|
|
1765
|
+
}, []);
|
|
1766
|
+
const adapter = useStableAdapter({
|
|
1767
|
+
apiUrl,
|
|
1768
|
+
buildContext,
|
|
1769
|
+
onToolCalls,
|
|
1770
|
+
captureState,
|
|
1771
|
+
restoreState,
|
|
1772
|
+
conversationId,
|
|
1773
|
+
takePending,
|
|
1774
|
+
registerImages,
|
|
1775
|
+
registerSender,
|
|
1776
|
+
getSelectedUser: () => selectedUserRef.current,
|
|
1777
|
+
onProvider,
|
|
1778
|
+
turnRef,
|
|
1779
|
+
appendAssistant
|
|
1780
|
+
});
|
|
1781
|
+
const runtime = (0, import_react3.useLocalRuntime)(adapter, { initialMessages });
|
|
1782
|
+
const imagesApi = (0, import_react2.useMemo)(
|
|
1783
|
+
() => ({ imagesByMessageId, registerImages }),
|
|
1784
|
+
[imagesByMessageId, registerImages]
|
|
1785
|
+
);
|
|
1786
|
+
const sendersApi = (0, import_react2.useMemo)(
|
|
1787
|
+
() => ({ senderByMessageId, registerSender }),
|
|
1788
|
+
[senderByMessageId, registerSender]
|
|
1789
|
+
);
|
|
1790
|
+
if (needsLogin) {
|
|
1791
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1792
|
+
ChatLoginScreen,
|
|
1793
|
+
{
|
|
1794
|
+
users: composableUsers,
|
|
1795
|
+
title,
|
|
1796
|
+
className,
|
|
1797
|
+
requirePassword,
|
|
1798
|
+
onSelect: handleSelectUser
|
|
1799
|
+
}
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.AssistantRuntimeProvider, { runtime, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChatUsersContext.Provider, { value: users ?? [], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AiUserContext.Provider, { value: aiUser, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageImagesContext.Provider, { value: imagesApi, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(MessageSendersContext.Provider, { value: sendersApi, children: [
|
|
1803
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1804
|
+
AssistantAppenderBinder,
|
|
1805
|
+
{
|
|
1806
|
+
appendRef: appendAssistantRef,
|
|
1807
|
+
aiUser,
|
|
1808
|
+
registerSender
|
|
1809
|
+
}
|
|
1810
|
+
),
|
|
1811
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1812
|
+
RemoteMessageSync,
|
|
1813
|
+
{
|
|
1814
|
+
apiUrl,
|
|
1815
|
+
conversationId,
|
|
1816
|
+
maintainerProUrl,
|
|
1817
|
+
maintainerProApiKey,
|
|
1818
|
+
aiUser,
|
|
1819
|
+
registerSender
|
|
1820
|
+
}
|
|
1821
|
+
),
|
|
1822
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1823
|
+
ChatUI,
|
|
1824
|
+
{
|
|
1825
|
+
apiUrl,
|
|
1826
|
+
title,
|
|
1827
|
+
emptyState,
|
|
1828
|
+
className,
|
|
1829
|
+
pendingImages,
|
|
1830
|
+
onAddImages,
|
|
1831
|
+
onRemoveImage,
|
|
1832
|
+
restoreState,
|
|
1833
|
+
turnRef,
|
|
1834
|
+
users,
|
|
1835
|
+
onSwitchUser: handleSwitchUser,
|
|
1836
|
+
conversationId,
|
|
1837
|
+
registerSender,
|
|
1838
|
+
getSelectedUser,
|
|
1839
|
+
onProvider
|
|
1840
|
+
}
|
|
1841
|
+
)
|
|
1842
|
+
] }) }) }) }) });
|
|
1843
|
+
}
|
|
1844
|
+
function AssistantAppenderBinder({
|
|
1845
|
+
appendRef,
|
|
1846
|
+
aiUser,
|
|
1847
|
+
registerSender
|
|
1848
|
+
}) {
|
|
1849
|
+
const threadRuntime = (0, import_react3.useThreadRuntime)();
|
|
1850
|
+
(0, import_react2.useEffect)(() => {
|
|
1851
|
+
appendRef.current = (text) => {
|
|
1852
|
+
const trimmed = text.trim();
|
|
1853
|
+
if (!trimmed) return;
|
|
1854
|
+
const beforeIds = new Set(
|
|
1855
|
+
threadRuntime.getState().messages.map((m) => m.id)
|
|
1856
|
+
);
|
|
1857
|
+
threadRuntime.append({
|
|
1858
|
+
role: "assistant",
|
|
1859
|
+
content: [{ type: "text", text: trimmed }],
|
|
1860
|
+
metadata: {
|
|
1861
|
+
custom: {
|
|
1862
|
+
senderType: "ai",
|
|
1863
|
+
senderName: aiUser.name
|
|
1864
|
+
}
|
|
1865
|
+
},
|
|
1866
|
+
startRun: false
|
|
1867
|
+
});
|
|
1868
|
+
const added = [...threadRuntime.getState().messages].reverse().find((m) => !beforeIds.has(m.id));
|
|
1869
|
+
if (added) {
|
|
1870
|
+
registerSender(added.id, aiUser);
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}, [appendRef, threadRuntime, aiUser, registerSender]);
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
function threadMessagePlainText(message) {
|
|
1877
|
+
const content = message.content;
|
|
1878
|
+
if (typeof content === "string") return content.trim();
|
|
1879
|
+
if (!Array.isArray(content)) return "";
|
|
1880
|
+
return content.map((part) => {
|
|
1881
|
+
if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
|
|
1882
|
+
return part.text;
|
|
1883
|
+
}
|
|
1884
|
+
return "";
|
|
1885
|
+
}).join("").trim();
|
|
1886
|
+
}
|
|
1887
|
+
function contentFingerprint(role, content) {
|
|
1888
|
+
return `${role}|${content.trim()}`;
|
|
1889
|
+
}
|
|
1890
|
+
function RemoteMessageSync({
|
|
1891
|
+
apiUrl,
|
|
1892
|
+
conversationId,
|
|
1893
|
+
maintainerProUrl,
|
|
1894
|
+
maintainerProApiKey,
|
|
1895
|
+
aiUser,
|
|
1896
|
+
registerSender,
|
|
1897
|
+
pollIntervalMs = 4e3
|
|
1898
|
+
}) {
|
|
1899
|
+
const threadRuntime = (0, import_react3.useThreadRuntime)();
|
|
1900
|
+
const seenRemoteIdsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
|
|
1901
|
+
const baselineReadyRef = (0, import_react2.useRef)(false);
|
|
1902
|
+
(0, import_react2.useEffect)(() => {
|
|
1903
|
+
let cancelled = false;
|
|
1904
|
+
let inFlight = false;
|
|
1905
|
+
baselineReadyRef.current = false;
|
|
1906
|
+
seenRemoteIdsRef.current = /* @__PURE__ */ new Set();
|
|
1907
|
+
if (maintainerProUrl && maintainerProApiKey && conversationId) {
|
|
1908
|
+
rememberConversationId(
|
|
1909
|
+
maintainerProUrl,
|
|
1910
|
+
maintainerProApiKey,
|
|
1911
|
+
conversationId
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
const sync = async () => {
|
|
1915
|
+
if (cancelled || inFlight) return;
|
|
1916
|
+
inFlight = true;
|
|
1917
|
+
try {
|
|
1918
|
+
const remote = await fetchChatMessages({
|
|
1919
|
+
apiUrl,
|
|
1920
|
+
conversationId,
|
|
1921
|
+
maintainerProUrl,
|
|
1922
|
+
maintainerProApiKey
|
|
1923
|
+
});
|
|
1924
|
+
if (cancelled) return;
|
|
1925
|
+
const local = threadRuntime.getState().messages;
|
|
1926
|
+
const localContents = new Set(
|
|
1927
|
+
local.map(
|
|
1928
|
+
(m) => contentFingerprint(m.role, threadMessagePlainText(m))
|
|
1929
|
+
)
|
|
1930
|
+
);
|
|
1931
|
+
if (!baselineReadyRef.current) {
|
|
1932
|
+
for (const m of remote) {
|
|
1933
|
+
if (!m.id) continue;
|
|
1934
|
+
const content = (m.content ?? "").trim();
|
|
1935
|
+
if (content && localContents.has(contentFingerprint(m.role, content))) {
|
|
1936
|
+
seenRemoteIdsRef.current.add(m.id);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
baselineReadyRef.current = true;
|
|
1940
|
+
}
|
|
1941
|
+
for (const m of remote) {
|
|
1942
|
+
if (m.role !== "user" && m.role !== "assistant") continue;
|
|
1943
|
+
const content = (m.content ?? "").trim();
|
|
1944
|
+
if (!content) continue;
|
|
1945
|
+
if (m.id && seenRemoteIdsRef.current.has(m.id)) continue;
|
|
1946
|
+
const key = contentFingerprint(m.role, content);
|
|
1947
|
+
if (localContents.has(key)) {
|
|
1948
|
+
if (m.id) seenRemoteIdsRef.current.add(m.id);
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (m.id) seenRemoteIdsRef.current.add(m.id);
|
|
1952
|
+
localContents.add(key);
|
|
1953
|
+
const sender = senderFromStoredMessage(m, aiUser);
|
|
1954
|
+
const beforeIds = new Set(
|
|
1955
|
+
threadRuntime.getState().messages.map((msg) => msg.id)
|
|
1956
|
+
);
|
|
1957
|
+
threadRuntime.append({
|
|
1958
|
+
role: m.role,
|
|
1959
|
+
content: [{ type: "text", text: content }],
|
|
1960
|
+
...sender ? {
|
|
1961
|
+
metadata: {
|
|
1962
|
+
custom: {
|
|
1963
|
+
senderType: sender.type,
|
|
1964
|
+
senderName: sender.name
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
} : {},
|
|
1968
|
+
startRun: false
|
|
1969
|
+
});
|
|
1970
|
+
const added = [...threadRuntime.getState().messages].reverse().find((msg) => !beforeIds.has(msg.id));
|
|
1971
|
+
if (added && sender) {
|
|
1972
|
+
registerSender(added.id, sender);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} catch {
|
|
1976
|
+
} finally {
|
|
1977
|
+
inFlight = false;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
void sync();
|
|
1981
|
+
const timer = setInterval(() => {
|
|
1982
|
+
void sync();
|
|
1983
|
+
}, pollIntervalMs);
|
|
1984
|
+
return () => {
|
|
1985
|
+
cancelled = true;
|
|
1986
|
+
clearInterval(timer);
|
|
1987
|
+
};
|
|
1988
|
+
}, [
|
|
1989
|
+
apiUrl,
|
|
1990
|
+
conversationId,
|
|
1991
|
+
maintainerProUrl,
|
|
1992
|
+
maintainerProApiKey,
|
|
1993
|
+
aiUser,
|
|
1994
|
+
registerSender,
|
|
1995
|
+
pollIntervalMs,
|
|
1996
|
+
threadRuntime
|
|
1997
|
+
]);
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/usePersistedChatOpen.ts
|
|
2002
|
+
var import_react4 = require("react");
|
|
2003
|
+
var DEFAULT_KEY = "ai-ui.chatOpen";
|
|
2004
|
+
function usePersistedChatOpen(storageKey = DEFAULT_KEY, defaultOpen = false) {
|
|
2005
|
+
const [open, setOpenState] = (0, import_react4.useState)(defaultOpen);
|
|
2006
|
+
(0, import_react4.useEffect)(() => {
|
|
2007
|
+
try {
|
|
2008
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
2009
|
+
if (raw === "true") setOpenState(true);
|
|
2010
|
+
else if (raw === "false") setOpenState(false);
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
}, [storageKey]);
|
|
2014
|
+
const setOpen = (0, import_react4.useCallback)(
|
|
2015
|
+
(value) => {
|
|
2016
|
+
setOpenState((prev) => {
|
|
2017
|
+
const next = typeof value === "function" ? value(prev) : value;
|
|
2018
|
+
try {
|
|
2019
|
+
window.localStorage.setItem(storageKey, String(next));
|
|
2020
|
+
} catch {
|
|
2021
|
+
}
|
|
2022
|
+
return next;
|
|
2023
|
+
});
|
|
2024
|
+
},
|
|
2025
|
+
[storageKey]
|
|
2026
|
+
);
|
|
2027
|
+
return [open, setOpen];
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/useChatUnread.ts
|
|
2031
|
+
var import_react5 = require("react");
|
|
2032
|
+
function useChatUnread(options) {
|
|
2033
|
+
const [unreadCount, setUnreadCount] = (0, import_react5.useState)(0);
|
|
2034
|
+
const openRef = (0, import_react5.useRef)(options.chatOpen);
|
|
2035
|
+
openRef.current = options.chatOpen;
|
|
2036
|
+
const pollerRef = (0, import_react5.useRef)(
|
|
2037
|
+
null
|
|
2038
|
+
);
|
|
2039
|
+
const {
|
|
2040
|
+
chatOpen,
|
|
2041
|
+
apiUrl,
|
|
2042
|
+
maintainerProUrl,
|
|
2043
|
+
maintainerProApiKey,
|
|
2044
|
+
conversationId,
|
|
2045
|
+
storageKey,
|
|
2046
|
+
pollIntervalMs
|
|
2047
|
+
} = options;
|
|
2048
|
+
(0, import_react5.useEffect)(() => {
|
|
2049
|
+
const poller = startChatUnreadPoller({
|
|
2050
|
+
apiUrl,
|
|
2051
|
+
maintainerProUrl,
|
|
2052
|
+
maintainerProApiKey,
|
|
2053
|
+
conversationId,
|
|
2054
|
+
storageKey,
|
|
2055
|
+
pollIntervalMs,
|
|
2056
|
+
isOpen: () => openRef.current,
|
|
2057
|
+
onChange: setUnreadCount
|
|
2058
|
+
});
|
|
2059
|
+
pollerRef.current = poller;
|
|
2060
|
+
return () => {
|
|
2061
|
+
poller.stop();
|
|
2062
|
+
pollerRef.current = null;
|
|
2063
|
+
};
|
|
2064
|
+
}, [
|
|
2065
|
+
apiUrl,
|
|
2066
|
+
maintainerProUrl,
|
|
2067
|
+
maintainerProApiKey,
|
|
2068
|
+
conversationId,
|
|
2069
|
+
storageKey,
|
|
2070
|
+
pollIntervalMs
|
|
2071
|
+
]);
|
|
2072
|
+
(0, import_react5.useEffect)(() => {
|
|
2073
|
+
if (chatOpen) {
|
|
2074
|
+
void pollerRef.current?.markRead();
|
|
2075
|
+
} else {
|
|
2076
|
+
void pollerRef.current?.refresh();
|
|
2077
|
+
}
|
|
2078
|
+
}, [chatOpen]);
|
|
2079
|
+
return {
|
|
2080
|
+
unreadCount,
|
|
2081
|
+
hasUnread: unreadCount > 0
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2085
|
+
0 && (module.exports = {
|
|
2086
|
+
AiChat,
|
|
2087
|
+
buildBrowserClientContext,
|
|
2088
|
+
countUnreadSince,
|
|
2089
|
+
createMaintainerProClient,
|
|
2090
|
+
fetchChatMessageTimestamps,
|
|
2091
|
+
fetchChatMessages,
|
|
2092
|
+
rememberConversationId,
|
|
2093
|
+
startChatUnreadPoller,
|
|
2094
|
+
useChatUnread,
|
|
2095
|
+
useClientContextBuilder,
|
|
2096
|
+
usePersistedChatOpen
|
|
2097
|
+
});
|