@aleph-alpha/chat-kit 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composables/fileUpload/index.d.ts +7 -0
- package/dist/composables/fileUpload/index.d.ts.map +1 -0
- package/dist/composables/fileUpload/types.d.ts +295 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts +4 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts +3 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -0
- package/dist/composables/index.d.ts +2 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +5 -2
- package/dist/index-BbTgIoYa.js +1557 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/package.json +1 -1
- package/src/composables/fileUpload/index.spec.ts +213 -0
- package/src/composables/fileUpload/index.ts +83 -0
- package/src/composables/fileUpload/types.ts +409 -0
- package/src/composables/fileUpload/useFileUpload.ts +1242 -0
- package/src/composables/fileUpload/useFileUploadWrapper.ts +382 -0
- package/src/composables/index.ts +2 -1
- package/src/index.ts +1 -17
- package/dist/useConversationSessions-GLZV_Ctr.js +0 -259
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { ref, computed, shallowRef, watch, toValue, type MaybeRef } from 'vue';
|
|
2
|
+
import type {
|
|
3
|
+
HttpClient,
|
|
4
|
+
RequestConfig,
|
|
5
|
+
SseStream,
|
|
6
|
+
SseEvent,
|
|
7
|
+
FileEntry,
|
|
8
|
+
ScopedFileView,
|
|
9
|
+
DocumentUploadConfig,
|
|
10
|
+
DocumentUploadProvider,
|
|
11
|
+
DocumentUploadReturn,
|
|
12
|
+
UploadEvent,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { useFileUpload } from './useFileUpload';
|
|
15
|
+
|
|
16
|
+
function buildHttpClient(
|
|
17
|
+
baseUrl: string,
|
|
18
|
+
defaultHeaders: Record<string, string>,
|
|
19
|
+
): HttpClient {
|
|
20
|
+
return {
|
|
21
|
+
async get<T>(url: string, config?: RequestConfig): Promise<T> {
|
|
22
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
23
|
+
const res = await fetch(fullUrl, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
signal: config?.signal,
|
|
26
|
+
headers: defaultHeaders,
|
|
27
|
+
credentials: 'include',
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error(`GET ${fullUrl} failed: ${res.status}`);
|
|
30
|
+
return res.json();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async post<T>(
|
|
34
|
+
url: string,
|
|
35
|
+
body: unknown,
|
|
36
|
+
config?: RequestConfig,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
39
|
+
const headers: Record<string, string> = { ...defaultHeaders };
|
|
40
|
+
let resolvedBody: BodyInit | undefined;
|
|
41
|
+
|
|
42
|
+
if (body instanceof FormData) {
|
|
43
|
+
resolvedBody = body;
|
|
44
|
+
} else {
|
|
45
|
+
headers['Content-Type'] = 'application/json';
|
|
46
|
+
resolvedBody = JSON.stringify(body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const res = await fetch(fullUrl, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
signal: config?.signal,
|
|
52
|
+
headers,
|
|
53
|
+
body: resolvedBody,
|
|
54
|
+
credentials: 'include',
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) throw new Error(`POST ${fullUrl} failed: ${res.status}`);
|
|
57
|
+
return res.json();
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async delete<T>(url: string, config?: RequestConfig): Promise<T> {
|
|
61
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
62
|
+
const res = await fetch(fullUrl, {
|
|
63
|
+
method: 'DELETE',
|
|
64
|
+
signal: config?.signal,
|
|
65
|
+
headers: defaultHeaders,
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`DELETE ${fullUrl} failed: ${res.status}`);
|
|
69
|
+
return res.json();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
stream(url: string, config?: RequestConfig): SseStream {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const signal = config?.signal
|
|
75
|
+
? AbortSignal.any([controller.signal, config.signal])
|
|
76
|
+
: controller.signal;
|
|
77
|
+
|
|
78
|
+
const listeners: Array<(event: SseEvent) => void> = [];
|
|
79
|
+
let closed = false;
|
|
80
|
+
|
|
81
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
82
|
+
const headers: Record<string, string> = {
|
|
83
|
+
Accept: 'text/event-stream',
|
|
84
|
+
...defaultHeaders,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function parseSseLines(
|
|
88
|
+
lines: string[],
|
|
89
|
+
notify: (event: SseEvent) => void,
|
|
90
|
+
): void {
|
|
91
|
+
let currentEvent = '';
|
|
92
|
+
let currentData = '';
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.startsWith('event:')) {
|
|
96
|
+
currentEvent = line.slice(6).trim();
|
|
97
|
+
} else if (line.startsWith('data:')) {
|
|
98
|
+
currentData = line.slice(5).trim();
|
|
99
|
+
} else if (line === '') {
|
|
100
|
+
if (currentData) {
|
|
101
|
+
notify({
|
|
102
|
+
event: currentEvent || 'message',
|
|
103
|
+
data: currentData,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
currentEvent = '';
|
|
107
|
+
currentData = '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(fullUrl, {
|
|
115
|
+
signal,
|
|
116
|
+
headers,
|
|
117
|
+
credentials: 'include',
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok || !response.body) return;
|
|
120
|
+
|
|
121
|
+
const reader = response.body.getReader();
|
|
122
|
+
const decoder = new TextDecoder();
|
|
123
|
+
let buffer = '';
|
|
124
|
+
|
|
125
|
+
while (!closed) {
|
|
126
|
+
const { done, value } = await reader.read();
|
|
127
|
+
if (done) break;
|
|
128
|
+
|
|
129
|
+
buffer += decoder.decode(value, { stream: true });
|
|
130
|
+
const lines = buffer.split('\n');
|
|
131
|
+
buffer = lines.pop() ?? '';
|
|
132
|
+
|
|
133
|
+
parseSseLines(lines, (event) => {
|
|
134
|
+
for (const cb of listeners) cb(event);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// stream ended or aborted
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
subscribe(callback: (event: SseEvent) => void) {
|
|
144
|
+
listeners.push(callback);
|
|
145
|
+
},
|
|
146
|
+
close() {
|
|
147
|
+
closed = true;
|
|
148
|
+
controller.abort();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createDocumentUploadProvider(
|
|
156
|
+
config: DocumentUploadConfig,
|
|
157
|
+
): DocumentUploadProvider {
|
|
158
|
+
const {
|
|
159
|
+
baseUrl,
|
|
160
|
+
userId,
|
|
161
|
+
workflowId,
|
|
162
|
+
searchStoreConfig,
|
|
163
|
+
workflowConfig,
|
|
164
|
+
persist = true,
|
|
165
|
+
maxConcurrency = 3,
|
|
166
|
+
} = config;
|
|
167
|
+
|
|
168
|
+
const defaultHeaders: Record<string, string> = {
|
|
169
|
+
'x-user-id': userId,
|
|
170
|
+
...(config.headers ?? {}),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const httpClient = buildHttpClient(baseUrl, defaultHeaders);
|
|
174
|
+
|
|
175
|
+
// ---- Search store resolution (one per user) ----
|
|
176
|
+
|
|
177
|
+
let resolvedStoreId: string | null = null;
|
|
178
|
+
let resolvePromise: Promise<string> | null = null;
|
|
179
|
+
|
|
180
|
+
async function createSearchStoreWithWorkflow(): Promise<string> {
|
|
181
|
+
const name = `${userId}-store-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
182
|
+
const storeRes = await httpClient.post<{ id: string }>('search-stores', {
|
|
183
|
+
name,
|
|
184
|
+
...searchStoreConfig,
|
|
185
|
+
metadata: { created_by: userId },
|
|
186
|
+
});
|
|
187
|
+
const newStoreId = storeRes.id;
|
|
188
|
+
|
|
189
|
+
await httpClient.post(
|
|
190
|
+
`search-stores/${newStoreId}/workflows`,
|
|
191
|
+
workflowConfig,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return newStoreId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function resolveSearchStore(): Promise<string> {
|
|
198
|
+
if (resolvedStoreId) return resolvedStoreId;
|
|
199
|
+
if (resolvePromise) return resolvePromise;
|
|
200
|
+
|
|
201
|
+
resolvePromise = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const stores = await httpClient.get<{ id: string }[]>('search-stores');
|
|
204
|
+
|
|
205
|
+
if (stores.length > 0 && stores[0]) {
|
|
206
|
+
resolvedStoreId = stores[0].id;
|
|
207
|
+
return resolvedStoreId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
resolvedStoreId = await createSearchStoreWithWorkflow();
|
|
211
|
+
return resolvedStoreId;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
resolvePromise = null;
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
return resolvePromise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Upload SDK singleton ----
|
|
222
|
+
|
|
223
|
+
let instance: ReturnType<typeof useFileUpload> | null = null;
|
|
224
|
+
|
|
225
|
+
function purgeInterruptedFiles() {
|
|
226
|
+
if (!instance) return;
|
|
227
|
+
const allFiles = [...instance.files.value.values()];
|
|
228
|
+
for (const entry of allFiles) {
|
|
229
|
+
if (entry.status === 'failed' && !entry.metadata.messageId) {
|
|
230
|
+
instance.removeFile(entry.id, { removeFromRemote: false });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getUploadInstance(): ReturnType<typeof useFileUpload> {
|
|
236
|
+
if (!instance) {
|
|
237
|
+
const onEvent = (event: UploadEvent) => {
|
|
238
|
+
config.onEvent?.(event);
|
|
239
|
+
if (event.type === 'state:restored') {
|
|
240
|
+
purgeInterruptedFiles();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
instance = useFileUpload({
|
|
245
|
+
client: httpClient,
|
|
246
|
+
workflowId,
|
|
247
|
+
persist,
|
|
248
|
+
maxConcurrency,
|
|
249
|
+
onEvent,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return instance;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- Public composable ----
|
|
256
|
+
|
|
257
|
+
function useDocumentUpload(
|
|
258
|
+
conversationIdRef: MaybeRef<string>,
|
|
259
|
+
): DocumentUploadReturn {
|
|
260
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
261
|
+
const upload = getUploadInstance();
|
|
262
|
+
const storeId = ref<string>(resolvedStoreId ?? '');
|
|
263
|
+
|
|
264
|
+
const currentScope = shallowRef<ScopedFileView | null>(
|
|
265
|
+
upload.scopeBy('conversationId', toValue(conversationIdRef)),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
watch(
|
|
269
|
+
() => toValue(conversationIdRef),
|
|
270
|
+
(newId: string, oldId: string) => {
|
|
271
|
+
if (oldId && oldId !== newId) {
|
|
272
|
+
const oldScope = upload.scopeBy('conversationId', oldId);
|
|
273
|
+
for (const entry of oldScope.files.value) {
|
|
274
|
+
upload.updateFileMetadata(entry.id, { conversationId: newId });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
currentScope.value = upload.scopeBy('conversationId', newId);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
function scopedFiles(): FileEntry[] {
|
|
282
|
+
return currentScope.value?.files.value ?? [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function uploadFiles(files: File[]) {
|
|
286
|
+
if (!files.length) return;
|
|
287
|
+
|
|
288
|
+
const userStoreId = await resolveSearchStore();
|
|
289
|
+
upload.setStoreId(userStoreId);
|
|
290
|
+
storeId.value = userStoreId;
|
|
291
|
+
|
|
292
|
+
const conversationId = toValue(conversationIdRef);
|
|
293
|
+
const result = await upload.upload({
|
|
294
|
+
id: `${conversationId}::${Date.now()}`,
|
|
295
|
+
files,
|
|
296
|
+
forwardedProps: { conversationId },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
for (const r of result.results) {
|
|
300
|
+
if (r.success && r.remoteFileId) {
|
|
301
|
+
upload.updateFileMetadata(r.fileId, {
|
|
302
|
+
data_platform_id: r.remoteFileId,
|
|
303
|
+
searchStoreId: storeId.value,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const successFileIds = result.results
|
|
309
|
+
.filter((r) => r.success && r.remoteFileId)
|
|
310
|
+
.map((r) => r.remoteFileId!);
|
|
311
|
+
|
|
312
|
+
if (successFileIds.length > 0) {
|
|
313
|
+
await upload.runWorkflow(successFileIds);
|
|
314
|
+
purgeCompletedFiles();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const unassignedFiles = computed(() =>
|
|
321
|
+
scopedFiles().filter((entry) => !entry.metadata.messageId),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
function assignFilesToMessage(messageId: string) {
|
|
325
|
+
for (const entry of scopedFiles()) {
|
|
326
|
+
if (!entry.metadata.messageId && entry.status === 'success') {
|
|
327
|
+
upload.updateFileMetadata(entry.id, { messageId });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getFilesForMessage(messageId: string): FileEntry[] {
|
|
333
|
+
return scopedFiles().filter(
|
|
334
|
+
(entry) =>
|
|
335
|
+
entry.metadata.messageId === messageId ||
|
|
336
|
+
(entry.metadata.responseId &&
|
|
337
|
+
messageId === `user-${entry.metadata.responseId}-0`),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function stampResponseId(responseId: string, localMessageId: string) {
|
|
342
|
+
for (const entry of scopedFiles()) {
|
|
343
|
+
if (entry.metadata.messageId === localMessageId) {
|
|
344
|
+
upload.updateFileMetadata(entry.id, { responseId });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function purgeCompletedFiles() {
|
|
350
|
+
const toPurge = scopedFiles().filter(
|
|
351
|
+
(entry) =>
|
|
352
|
+
entry.metadata.messageId &&
|
|
353
|
+
(entry.status === 'success' || entry.status === 'failed'),
|
|
354
|
+
);
|
|
355
|
+
for (const entry of toPurge) {
|
|
356
|
+
upload.removeFile(entry.id, { removeFromRemote: false });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleFilesSelected(event: Event) {
|
|
361
|
+
const input = event.target as HTMLInputElement;
|
|
362
|
+
const fileList = Array.from(input.files ?? []);
|
|
363
|
+
uploadFiles(fileList);
|
|
364
|
+
input.value = '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
upload,
|
|
369
|
+
storeId,
|
|
370
|
+
fileInput,
|
|
371
|
+
uploadFiles,
|
|
372
|
+
unassignedFiles,
|
|
373
|
+
assignFilesToMessage,
|
|
374
|
+
getFilesForMessage,
|
|
375
|
+
stampResponseId,
|
|
376
|
+
purgeCompletedFiles,
|
|
377
|
+
handleFilesSelected,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { useDocumentUpload };
|
|
382
|
+
}
|
package/src/composables/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -28,23 +28,7 @@ export {
|
|
|
28
28
|
export type { ChatKitLabels } from './handlers';
|
|
29
29
|
|
|
30
30
|
// Composables
|
|
31
|
-
export
|
|
32
|
-
useChat,
|
|
33
|
-
useConversationSessions,
|
|
34
|
-
useConversations,
|
|
35
|
-
} from './composables';
|
|
36
|
-
export type {
|
|
37
|
-
ChatActions,
|
|
38
|
-
ChatState,
|
|
39
|
-
ConversationSession,
|
|
40
|
-
CreateConversationInput,
|
|
41
|
-
MessageHandler,
|
|
42
|
-
MutationOptions,
|
|
43
|
-
UseChatReturn,
|
|
44
|
-
UseConversationSessionsReturn,
|
|
45
|
-
UseConversationsOptions,
|
|
46
|
-
UseConversationsReturn,
|
|
47
|
-
} from './composables';
|
|
31
|
+
export * from './composables';
|
|
48
32
|
|
|
49
33
|
// Components
|
|
50
34
|
export {
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { g as generateId } from "./generateId-BHf0W4Rq.js";
|
|
2
|
-
import { ref, shallowRef, computed, readonly } from "vue";
|
|
3
|
-
import { u as useMessageHandler } from "./useMessageHandler-ZRC-gIOJ.js";
|
|
4
|
-
import "./purify.es-md74IFlY.js";
|
|
5
|
-
function secondsToDate(seconds) {
|
|
6
|
-
return new Date(seconds * 1e3);
|
|
7
|
-
}
|
|
8
|
-
function toError(value) {
|
|
9
|
-
return value instanceof Error ? value : new Error(String(value));
|
|
10
|
-
}
|
|
11
|
-
function useChat() {
|
|
12
|
-
}
|
|
13
|
-
const DEFAULT_PAGE_SIZE = 20;
|
|
14
|
-
function useConversations(options) {
|
|
15
|
-
const http = options.httpClient;
|
|
16
|
-
const path = options.path;
|
|
17
|
-
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
18
|
-
const conversations = ref([]);
|
|
19
|
-
const inflightCount = shallowRef(0);
|
|
20
|
-
const isLoading = computed(() => inflightCount.value > 0);
|
|
21
|
-
const hasMore = shallowRef(false);
|
|
22
|
-
const error = shallowRef(null);
|
|
23
|
-
let nextCursor = null;
|
|
24
|
-
function beginRequest() {
|
|
25
|
-
inflightCount.value++;
|
|
26
|
-
error.value = null;
|
|
27
|
-
}
|
|
28
|
-
function endRequest() {
|
|
29
|
-
inflightCount.value--;
|
|
30
|
-
}
|
|
31
|
-
async function fetchPage(params) {
|
|
32
|
-
const query = {
|
|
33
|
-
limit: params.limit,
|
|
34
|
-
order: "desc"
|
|
35
|
-
};
|
|
36
|
-
if (params.cursor) query.after = params.cursor;
|
|
37
|
-
const raw = await http.get(path, { params: query });
|
|
38
|
-
return mapConversationList(raw);
|
|
39
|
-
}
|
|
40
|
-
async function fetch(opts) {
|
|
41
|
-
beginRequest();
|
|
42
|
-
try {
|
|
43
|
-
const page = await fetchPage({
|
|
44
|
-
limit: opts?.limit ?? pageSize,
|
|
45
|
-
cursor: null
|
|
46
|
-
});
|
|
47
|
-
conversations.value = page.items;
|
|
48
|
-
nextCursor = page.nextCursor;
|
|
49
|
-
hasMore.value = page.nextCursor !== null;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
error.value = toError(e);
|
|
52
|
-
throw error.value;
|
|
53
|
-
} finally {
|
|
54
|
-
endRequest();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function fetchMore() {
|
|
58
|
-
if (!hasMore.value || nextCursor === null) return;
|
|
59
|
-
beginRequest();
|
|
60
|
-
try {
|
|
61
|
-
const page = await fetchPage({ limit: pageSize, cursor: nextCursor });
|
|
62
|
-
conversations.value = [...conversations.value, ...page.items];
|
|
63
|
-
nextCursor = page.nextCursor;
|
|
64
|
-
hasMore.value = page.nextCursor !== null;
|
|
65
|
-
} catch (e) {
|
|
66
|
-
error.value = toError(e);
|
|
67
|
-
throw error.value;
|
|
68
|
-
} finally {
|
|
69
|
-
endRequest();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async function fetchById(id) {
|
|
73
|
-
beginRequest();
|
|
74
|
-
try {
|
|
75
|
-
const raw = await http.get(`${path}/${id}`);
|
|
76
|
-
return mapConversation(raw);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
error.value = toError(e);
|
|
79
|
-
throw error.value;
|
|
80
|
-
} finally {
|
|
81
|
-
endRequest();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async function create(input, opts) {
|
|
85
|
-
const metadata = { ...input?.metadata ?? {} };
|
|
86
|
-
if (input?.title !== void 0) metadata.title = input.title;
|
|
87
|
-
const body = { metadata };
|
|
88
|
-
const previous = conversations.value;
|
|
89
|
-
let placeholder = null;
|
|
90
|
-
if (opts?.optimistic) {
|
|
91
|
-
placeholder = makePlaceholder(input?.title);
|
|
92
|
-
conversations.value = [placeholder, ...previous];
|
|
93
|
-
}
|
|
94
|
-
beginRequest();
|
|
95
|
-
try {
|
|
96
|
-
const raw = await http.post(path, body);
|
|
97
|
-
const created = mapConversation(raw);
|
|
98
|
-
conversations.value = placeholder ? conversations.value.map(
|
|
99
|
-
(c) => c.id === placeholder.id ? created : c
|
|
100
|
-
) : [created, ...conversations.value];
|
|
101
|
-
return created;
|
|
102
|
-
} catch (e) {
|
|
103
|
-
if (placeholder) conversations.value = previous;
|
|
104
|
-
error.value = toError(e);
|
|
105
|
-
throw error.value;
|
|
106
|
-
} finally {
|
|
107
|
-
endRequest();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async function rename(id, title, opts) {
|
|
111
|
-
const previous = conversations.value;
|
|
112
|
-
if (opts?.optimistic) {
|
|
113
|
-
conversations.value = previous.map(
|
|
114
|
-
(c) => c.id === id ? { ...c, title, updatedAt: /* @__PURE__ */ new Date() } : c
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
beginRequest();
|
|
118
|
-
try {
|
|
119
|
-
const raw = await http.post(`${path}/${id}`, {
|
|
120
|
-
metadata: { title }
|
|
121
|
-
});
|
|
122
|
-
const updated = mapConversation(raw);
|
|
123
|
-
conversations.value = conversations.value.map(
|
|
124
|
-
(c) => c.id === id ? updated : c
|
|
125
|
-
);
|
|
126
|
-
return updated;
|
|
127
|
-
} catch (e) {
|
|
128
|
-
if (opts?.optimistic) conversations.value = previous;
|
|
129
|
-
error.value = toError(e);
|
|
130
|
-
throw error.value;
|
|
131
|
-
} finally {
|
|
132
|
-
endRequest();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async function remove(id, opts) {
|
|
136
|
-
const previous = conversations.value;
|
|
137
|
-
if (opts?.optimistic) {
|
|
138
|
-
conversations.value = previous.filter((c) => c.id !== id);
|
|
139
|
-
}
|
|
140
|
-
beginRequest();
|
|
141
|
-
try {
|
|
142
|
-
await http.delete(`${path}/${id}`);
|
|
143
|
-
if (!opts?.optimistic) {
|
|
144
|
-
conversations.value = previous.filter((c) => c.id !== id);
|
|
145
|
-
}
|
|
146
|
-
} catch (e) {
|
|
147
|
-
if (opts?.optimistic) conversations.value = previous;
|
|
148
|
-
error.value = toError(e);
|
|
149
|
-
throw error.value;
|
|
150
|
-
} finally {
|
|
151
|
-
endRequest();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function getById(id) {
|
|
155
|
-
return conversations.value.find((c) => c.id === id);
|
|
156
|
-
}
|
|
157
|
-
async function fetchResponses(id, opts) {
|
|
158
|
-
beginRequest();
|
|
159
|
-
try {
|
|
160
|
-
const raw = await http.get(`${path}/${id}/responses`, {
|
|
161
|
-
params: { order: opts?.order ?? "asc" }
|
|
162
|
-
});
|
|
163
|
-
return raw.data;
|
|
164
|
-
} catch (e) {
|
|
165
|
-
error.value = toError(e);
|
|
166
|
-
throw error.value;
|
|
167
|
-
} finally {
|
|
168
|
-
endRequest();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
conversations: readonly(conversations),
|
|
173
|
-
isLoading,
|
|
174
|
-
hasMore: readonly(hasMore),
|
|
175
|
-
error: readonly(error),
|
|
176
|
-
fetch,
|
|
177
|
-
fetchMore,
|
|
178
|
-
fetchById,
|
|
179
|
-
create,
|
|
180
|
-
rename,
|
|
181
|
-
remove,
|
|
182
|
-
getById,
|
|
183
|
-
fetchResponses
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function mapConversation(raw) {
|
|
187
|
-
return {
|
|
188
|
-
id: raw.id,
|
|
189
|
-
title: raw.metadata?.title ?? "",
|
|
190
|
-
createdAt: secondsToDate(raw.created_at),
|
|
191
|
-
updatedAt: secondsToDate(raw.updated_at)
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function mapConversationList(raw) {
|
|
195
|
-
return {
|
|
196
|
-
items: raw.data.map(mapConversation),
|
|
197
|
-
nextCursor: raw.has_more ? raw.last_id ?? null : null
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function makePlaceholder(title) {
|
|
201
|
-
const now = /* @__PURE__ */ new Date();
|
|
202
|
-
return {
|
|
203
|
-
id: `pending-${generateId()}`,
|
|
204
|
-
title: title ?? "",
|
|
205
|
-
createdAt: now,
|
|
206
|
-
updatedAt: now
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function useConversationSessions() {
|
|
210
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
211
|
-
const focusedId = ref(null);
|
|
212
|
-
function createSession(conversationId) {
|
|
213
|
-
const existing = sessions.get(conversationId);
|
|
214
|
-
if (existing) return existing;
|
|
215
|
-
const session = {
|
|
216
|
-
id: conversationId,
|
|
217
|
-
messageHandler: useMessageHandler()
|
|
218
|
-
};
|
|
219
|
-
sessions.set(conversationId, session);
|
|
220
|
-
return session;
|
|
221
|
-
}
|
|
222
|
-
function getSession(conversationId) {
|
|
223
|
-
return sessions.get(conversationId);
|
|
224
|
-
}
|
|
225
|
-
function setActiveConversation(conversationId) {
|
|
226
|
-
if (conversationId === null) {
|
|
227
|
-
focusedId.value = null;
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
createSession(conversationId);
|
|
231
|
-
focusedId.value = conversationId;
|
|
232
|
-
}
|
|
233
|
-
function removeSession(conversationId) {
|
|
234
|
-
sessions.delete(conversationId);
|
|
235
|
-
if (focusedId.value === conversationId) {
|
|
236
|
-
focusedId.value = null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const activeSession = computed(() => {
|
|
240
|
-
const id = focusedId.value;
|
|
241
|
-
return id !== null ? sessions.get(id) ?? null : null;
|
|
242
|
-
});
|
|
243
|
-
const messageHandler = computed(
|
|
244
|
-
() => activeSession.value?.messageHandler ?? null
|
|
245
|
-
);
|
|
246
|
-
return {
|
|
247
|
-
activeSession,
|
|
248
|
-
messageHandler,
|
|
249
|
-
createSession,
|
|
250
|
-
getSession,
|
|
251
|
-
setActiveConversation,
|
|
252
|
-
removeSession
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
export {
|
|
256
|
-
useConversationSessions as a,
|
|
257
|
-
useConversations as b,
|
|
258
|
-
useChat as u
|
|
259
|
-
};
|