@baishuyun/chat-sdk 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/chat-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "",
5
5
  "main": "src/index.jsx",
6
6
  "module": "dist/chat-sdk.js",
@@ -11,7 +11,7 @@ import {
11
11
  useRef,
12
12
  useState,
13
13
  } from 'react';
14
- import { cn } from '@/lib/utils';
14
+ import { cn, uploadFile } from '@/lib/utils';
15
15
  import {
16
16
  PromptInput,
17
17
  PromptInputSubmit,
@@ -154,28 +154,10 @@ function PureMultimodalInput({
154
154
 
155
155
  const toast = useToast();
156
156
 
157
- const uploadFile = useCallback(async (file: File, signal?: AbortSignal) => {
158
- const formData = new FormData();
159
- formData.append('file', file);
160
-
161
- try {
162
- const response = await fetch(fileUploadEndpoint, {
163
- method: 'POST',
164
- body: formData,
165
- signal,
166
- });
167
-
168
- if (response.ok) {
169
- const data = await response.json();
170
-
171
- console.log(data);
172
-
173
- return data;
174
- }
175
- const { error } = await response.json();
176
- console.error('upload error', error);
177
- } catch (_error) {}
178
- }, []);
157
+ const upload = useCallback(
158
+ (file: File, signal?: AbortSignal) => uploadFile(file, fileUploadEndpoint, signal),
159
+ [fileUploadEndpoint]
160
+ );
179
161
 
180
162
  const handleFileChange = useCallback(
181
163
  async (event: ChangeEvent<HTMLInputElement>) => {
@@ -208,7 +190,7 @@ function PureMultimodalInput({
208
190
  const uploadPromises = files.map((file) => {
209
191
  const controller = new AbortController();
210
192
  uploadAbortControllers.current.set(file.name, controller);
211
- return uploadFile(file, controller.signal);
193
+ return upload(file, controller.signal);
212
194
  });
213
195
  const uploadedAttachments = await Promise.all(uploadPromises);
214
196
  const successfullyUploadedAttachments = uploadedAttachments.filter(
@@ -228,7 +210,7 @@ function PureMultimodalInput({
228
210
  uploadAbortControllers.current.clear();
229
211
  }
230
212
  },
231
- [setAttachments, uploadFile, attachments]
213
+ [setAttachments, upload, attachments]
232
214
  );
233
215
 
234
216
  const handlePaste = useCallback(
@@ -253,7 +235,7 @@ function PureMultimodalInput({
253
235
  const uploadPromises = imageItems
254
236
  .map((item) => item.getAsFile())
255
237
  .filter((file): file is File => file !== null)
256
- .map((file) => uploadFile(file));
238
+ .map((file) => upload(file));
257
239
 
258
240
  const uploadedAttachments = await Promise.all(uploadPromises);
259
241
  const successfullyUploadedAttachments = uploadedAttachments.filter(
@@ -270,7 +252,7 @@ function PureMultimodalInput({
270
252
  setUploadQueue([]);
271
253
  }
272
254
  },
273
- [setAttachments, uploadFile]
255
+ [setAttachments, upload]
274
256
  );
275
257
 
276
258
  const accept = (useChatPreference()?.acceptAttachmentFileType || [
package/src/lib/utils.ts CHANGED
@@ -2,7 +2,7 @@ import { clsx, type ClassValue } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
  import { UIDataTypes, UIMessagePart, UITools } from 'ai';
4
4
  import { Field } from '@/plugins/form-builder-base-plugin/types';
5
- import { IFilledField, IQueryResult } from '@baishuyun/types';
5
+ import { Attachment, IFilledField, IQueryResult } from '@baishuyun/types';
6
6
  import { FORM_ICON_OPTIONS } from '@/const/ui';
7
7
 
8
8
  export function cn(...inputs: ClassValue[]) {
@@ -418,3 +418,72 @@ export const formatJSONIfValid = (str: string): string | null => {
418
418
  return null;
419
419
  }
420
420
  };
421
+
422
+ /**
423
+ * 从 URL 下载文件,返回 File 对象
424
+ */
425
+ export const downloadFile = async (url: string): Promise<File> => {
426
+ const response = await fetch(url);
427
+ if (!response.ok) {
428
+ throw new Error(`Failed to download file from ${url}: ${response.status}`);
429
+ }
430
+ const blob = await response.blob();
431
+ const contentType = response.headers.get('content-type') || 'application/octet-stream';
432
+ const filename = new URL(url).pathname.split('/').pop() || 'attachment';
433
+ return new File([blob], filename, { type: contentType });
434
+ };
435
+
436
+ /**
437
+ * 上传文件到指定 endpoint,返回 Attachment 信息
438
+ */
439
+ export const uploadFile = async (
440
+ file: File,
441
+ uploadEndpoint: string,
442
+ signal?: AbortSignal
443
+ ): Promise<Attachment | undefined> => {
444
+ const formData = new FormData();
445
+ formData.append('file', file);
446
+
447
+ try {
448
+ const response = await fetch(uploadEndpoint, {
449
+ method: 'POST',
450
+ body: formData,
451
+ signal,
452
+ });
453
+
454
+ if (response.ok) {
455
+ return await response.json();
456
+ }
457
+ const { error } = await response.json();
458
+ console.error('upload error', error);
459
+ return undefined;
460
+ } catch (error) {
461
+ console.error('Error uploading file:', error);
462
+ return undefined;
463
+ }
464
+ };
465
+
466
+ export const urls2fileParts = async (attachmentsUrl: string[], uploadEndpoint: string) => {
467
+ if (!attachmentsUrl?.length) {
468
+ return [];
469
+ }
470
+
471
+ const results = await Promise.all(
472
+ attachmentsUrl.map(async (url) => {
473
+ const file = await downloadFile(url);
474
+ return uploadFile(file, uploadEndpoint);
475
+ })
476
+ );
477
+
478
+ return results
479
+ .filter((attachment): attachment is Attachment => attachment !== undefined)
480
+ .map((attachment) => ({
481
+ type: 'file' as const,
482
+ url: attachment.url,
483
+ name: attachment.name,
484
+ mediaType: attachment.contentType,
485
+ providerMetadata: {
486
+ uploadMetadata: attachment,
487
+ },
488
+ }));
489
+ };
package/src/sdk.impl.tsx CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  import { EvtBus } from './lib/event-emitter';
14
14
  import type { UseChatHelpers } from '@ai-sdk/react';
15
15
  import { ChatRequestOptions, JSONValue } from 'ai';
16
+ import { ensureEndSlash, urls2fileParts } from './lib/utils';
16
17
 
17
18
  export class ChatSDK implements IChatSDK {
18
19
  static instances: ChatSDK[] = [];
@@ -116,16 +117,38 @@ export class ChatSDK implements IChatSDK {
116
117
  this._appendFakeBotMessage?.(text);
117
118
  }
118
119
 
119
- public sendMessage = async (text: string, customVariable: JSONValue, options: ChatRequestOptions) => {
120
+ public sendMessage = async (text: string, customVariable: JSONValue, options: ChatRequestOptions & {
121
+ attachmentsUrl?: string[];
122
+ } = {}) => {
120
123
  await this.whenChatComponentReady();
121
124
 
122
- // if is streaming, return;
125
+ const { attachmentsUrl, ...restOptions } = options;
126
+
127
+ const uploadEndpoint = `${ensureEndSlash(this.options.backendApiEndpoint!)}form/attachment/upload`;
128
+
129
+ if (attachmentsUrl && attachmentsUrl.length > 0) {
130
+ this.Store.getState().setGlobalFakeLoadingMessage("正在处理附件...");
131
+ }
132
+
133
+ let fileParts: ChatMessage['parts'] = await urls2fileParts(attachmentsUrl || [], uploadEndpoint);
134
+ this.Store.getState().clearGlobalFakeLoadingMessage();
135
+
136
+ const msg = fileParts.length > 0
137
+ ? {
138
+ role: 'user',
139
+ parts: [
140
+ ...fileParts,
141
+ { type: 'text' as const, text },
142
+ ],
143
+ } as ChatMessage
144
+ : { text };
145
+
123
146
  this._sendMessage?.(
124
- { text }, // First arg: message data
125
- { // Second arg: options
126
- ...options,
147
+ msg,
148
+ {
149
+ ...restOptions,
127
150
  body: {
128
- ...(options?.body || {}),
151
+ ...(restOptions?.body || {}),
129
152
  userVar: customVariable,
130
153
  },
131
154
  }