@meetsmore-oss/use-ai-client 1.15.0 → 1.16.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/index.d.ts CHANGED
@@ -668,24 +668,40 @@ interface FileAttachment {
668
668
  transformedContent?: string;
669
669
  }
670
670
  /**
671
- * Abstract file upload backend interface.
672
- * Converts File objects to URLs at send time.
671
+ * Result of preparing a file for send.
672
+ *
673
+ * Only one of the two is returned:
674
+ * - `{ url }`: a directly usable url (an inline base64 data URL, or a remote url).
675
+ * The bytes travel along with the message.
676
+ * - `{ ref }`: a pointer into persistent storage (e.g. an S3 key). The bytes stay in
677
+ * storage; only the ref travels with the message and is kept in history. It is
678
+ * resolved on the server before being passed to the model.
679
+ *
680
+ * A backend may also return a bare `string`, which is treated as `{ url: string }`
681
+ * (for backward compatibility with backends written against the older signature).
682
+ */
683
+ type FileUploadResult = {
684
+ url: string;
685
+ } | {
686
+ ref: string;
687
+ };
688
+ /**
689
+ * Interface for an abstract file upload backend.
690
+ * Converts a File object into a sendable reference at send time.
673
691
  *
674
692
  * Implementations:
675
- * - EmbedFileUploadBackend: Converts to base64 data URL (built-in)
676
- * - S3FileUploadBackend: Uploads to S3 and returns public URL (future)
693
+ * - EmbedFileUploadBackend: converts to a base64 data URL (built-in)
694
+ * - S3FileUploadBackend: uploads to storage and returns a `ref` (future)
677
695
  */
678
696
  interface FileUploadBackend {
679
697
  /**
680
- * Prepare file for sending to AI.
681
- * Called at send time - converts File to URL.
698
+ * Prepare a file to send to the AI. Called at send time.
682
699
  *
683
700
  * @param file - The File object to prepare
684
- * @returns Promise resolving to a URL string
685
- * - For embed: base64 data URL
686
- * - For S3: public URL after upload
701
+ * @returns A {@link FileUploadResult} (`{ url }` or `{ ref }`), or a bare url string
702
+ * for backward compatibility (treated as `{ url }`).
687
703
  */
688
- prepareForSend(file: File): Promise<string>;
704
+ prepareForSend(file: File): Promise<string | FileUploadResult>;
689
705
  }
690
706
  /**
691
707
  * Context provided to file transformers.
@@ -773,6 +789,14 @@ interface FileUploadConfig {
773
789
  * before being sent to the AI.
774
790
  */
775
791
  transformers?: FileTransformerMap;
792
+ /**
793
+ * Maximum number of attachments allowed per message.
794
+ * Enforced at attach time; files beyond the limit are rejected with an error.
795
+ * The value is the host's policy (e.g. set it below the AI provider's per-message
796
+ * limit). use-ai only exposes the knob and assumes no default. When undefined, the
797
+ * number of attachments is not limited.
798
+ */
799
+ maxAttachments?: number;
776
800
  }
777
801
 
778
802
  /**
@@ -811,11 +835,35 @@ interface PersistedTransformedFileContent {
811
835
  text: string;
812
836
  originalFile: PersistedFileMetadata;
813
837
  }
838
+ /**
839
+ * Ref-backed attachment content part for persisted messages.
840
+ *
841
+ * Unlike {@link PersistedFileContent} (metadata only, discarded on reload), this part
842
+ * holds a `ref` into persistent storage (e.g. an S3 key). This lets attachments be
843
+ * re-sent to the AI even after a page reload. The actual bytes live in storage rather
844
+ * than localStorage, and the ref is resolved on the host before the next run.
845
+ *
846
+ * Covers both image and non-image attachments in a single type (the persistence layer
847
+ * does not split them: display is identical, and the re-send tag is derived from
848
+ * `mimeType`). On reload it is read two ways: as a name/size chip for display, and as a
849
+ * re-sendable `{ type: 'image_ref' | 'file_ref', ref }` wire part (see `messageConversion.ts`).
850
+ */
851
+ interface PersistedAttachmentRefContent {
852
+ type: 'attachment_ref';
853
+ /** Ref into persistent storage (e.g. an S3 key). Never expires. */
854
+ ref: string;
855
+ /** Original file name (for display). */
856
+ name: string;
857
+ /** MIME type, e.g. 'image/jpeg' | 'application/pdf'. Decides whether to re-send as image or file. */
858
+ mimeType: string;
859
+ /** Byte size after client-side resize (for display). */
860
+ size: number;
861
+ }
814
862
  /**
815
863
  * Content part for persisted messages.
816
- * Can be text, file metadata, or transformed file content.
864
+ * One of: text, file metadata, transformed file content, or a ref-backed attachment.
817
865
  */
818
- type PersistedContentPart = PersistedTextContent | PersistedFileContent | PersistedTransformedFileContent;
866
+ type PersistedContentPart = PersistedTextContent | PersistedFileContent | PersistedTransformedFileContent | PersistedAttachmentRefContent;
819
867
  /**
820
868
  * Content that can be persisted.
821
869
  * Simple string for text-only messages, or array for multimodal content.
@@ -1200,6 +1248,8 @@ declare const defaultStrings: {
1200
1248
  fileSizeError: string;
1201
1249
  /** File type error (use {type} placeholder) */
1202
1250
  fileTypeError: string;
1251
+ /** Error when too many files are attached (use {max} placeholder) */
1252
+ maxAttachmentsError: string;
1203
1253
  };
1204
1254
  floatingButton: {
1205
1255
  /** Floating button title when connected */
@@ -2638,4 +2688,4 @@ interface UseDropdownStateOptions {
2638
2688
  */
2639
2689
  declare function useDropdownState(options?: UseDropdownStateOptions): UseDropdownStateReturn;
2640
2690
 
2641
- export { type AgentContextValue, type Chat, type ChatContextValue, type ChatMetadata, type ChatPanelProps, type ChatRepository, CloseButton, type CommandContextValue, type CommandRepository, type CreateChatOptions, type CreateCommandOptions, DEFAULT_MAX_FILE_SIZE, type DefinedTool, type DropZoneProps, EmbedFileUploadBackend, type EnabledFeatures, type ExecutingToolDisplay, type FileAttachment, type FileProcessingState, type FileProcessingStatus, type FileTransformer, type FileTransformerContext, type FileTransformerMap, type FileUploadBackend, type FileUploadConfig, type FloatingButtonProps, type InlineSaveProps, type ListChatsOptions, type ListCommandsOptions, LocalStorageChatRepository, LocalStorageCommandRepository, type Message, type PendingToolApproval, type PersistedContentPart, type PersistedFileContent, type PersistedFileMetadata, type PersistedMessage, type PersistedMessageContent, type PersistedTextContent, type ProcessAttachmentsConfig, type PromptsContextValue, type RegisterToolsOptions, type SavedCommand, type SendMessageOptions, type SubmitMode, type ToolExecutionContext, type ToolOptions, type ToolRegistryContextValue, type ToolsDefinition, type TriggerWorkflowOptions, UseAIChat, UseAIChatPanel, type UseAIChatPanelProps, type UseAIChatPanelStrings, type UseAIChatPanelTheme, type UseAIChatProps, UseAIClient, type UseAIConfig, type UseAIContextValue, UseAIFloatingButton, UseAIFloatingChatWrapper, type UseAIOptions, UseAIProvider, type UseAIProviderProps, type UseAIResult, type UseAIStrings, type UseAITheme, type UseAIWorkflowResult, type UseAgentSelectionOptions, type UseAgentSelectionReturn, type UseChatManagementOptions, type UseChatManagementReturn, type UseCommandManagementOptions, type UseCommandManagementReturn, type UseDropdownStateOptions, type UseDropdownStateReturn, type UseFeedbackOptions, type UseFeedbackReturn, type UseFileUploadOptions, type UseFileUploadReturn, type UseMessageQueueOptions, type UseMessageQueueReturn, type UsePromptStateOptions, type UsePromptStateReturn, type UseServerEventsOptions, type UseServerEventsReturn, type UseSlashCommandsOptions, type UseSlashCommandsReturn, type UseToolSystemOptions, type UseToolSystemReturn, type WorkflowProgress, clearTransformationCache, convertToolsToDefinitions, defaultStrings, defaultTheme, defineTool, executeDefinedTool, findTransformerPattern, generateChatId, generateCommandId, generateMessageId, matchesMimeType, processAttachments, useAI, useAIContext, useAIWorkflow, useAgentSelection, useChatManagement, useCommandManagement, useDropdownState, useFeedback, useFileUpload, useMessageQueue, usePromptState, useServerEvents, useSlashCommands, useStableTools, useStrings, useTheme, useToolSystem, validateCommandName };
2691
+ export { type AgentContextValue, type Chat, type ChatContextValue, type ChatMetadata, type ChatPanelProps, type ChatRepository, CloseButton, type CommandContextValue, type CommandRepository, type CreateChatOptions, type CreateCommandOptions, DEFAULT_MAX_FILE_SIZE, type DefinedTool, type DropZoneProps, EmbedFileUploadBackend, type EnabledFeatures, type ExecutingToolDisplay, type FileAttachment, type FileProcessingState, type FileProcessingStatus, type FileTransformer, type FileTransformerContext, type FileTransformerMap, type FileUploadBackend, type FileUploadConfig, type FileUploadResult, type FloatingButtonProps, type InlineSaveProps, type ListChatsOptions, type ListCommandsOptions, LocalStorageChatRepository, LocalStorageCommandRepository, type Message, type PendingToolApproval, type PersistedAttachmentRefContent, type PersistedContentPart, type PersistedFileContent, type PersistedFileMetadata, type PersistedMessage, type PersistedMessageContent, type PersistedTextContent, type PersistedTransformedFileContent, type ProcessAttachmentsConfig, type PromptsContextValue, type RegisterToolsOptions, type SavedCommand, type SendMessageOptions, type SubmitMode, type ToolExecutionContext, type ToolOptions, type ToolRegistryContextValue, type ToolsDefinition, type TriggerWorkflowOptions, UseAIChat, UseAIChatPanel, type UseAIChatPanelProps, type UseAIChatPanelStrings, type UseAIChatPanelTheme, type UseAIChatProps, UseAIClient, type UseAIConfig, type UseAIContextValue, UseAIFloatingButton, UseAIFloatingChatWrapper, type UseAIOptions, UseAIProvider, type UseAIProviderProps, type UseAIResult, type UseAIStrings, type UseAITheme, type UseAIWorkflowResult, type UseAgentSelectionOptions, type UseAgentSelectionReturn, type UseChatManagementOptions, type UseChatManagementReturn, type UseCommandManagementOptions, type UseCommandManagementReturn, type UseDropdownStateOptions, type UseDropdownStateReturn, type UseFeedbackOptions, type UseFeedbackReturn, type UseFileUploadOptions, type UseFileUploadReturn, type UseMessageQueueOptions, type UseMessageQueueReturn, type UsePromptStateOptions, type UsePromptStateReturn, type UseServerEventsOptions, type UseServerEventsReturn, type UseSlashCommandsOptions, type UseSlashCommandsReturn, type UseToolSystemOptions, type UseToolSystemReturn, type WorkflowProgress, clearTransformationCache, convertToolsToDefinitions, defaultStrings, defaultTheme, defineTool, executeDefinedTool, findTransformerPattern, generateChatId, generateCommandId, generateMessageId, matchesMimeType, processAttachments, useAI, useAIContext, useAIWorkflow, useAgentSelection, useChatManagement, useCommandManagement, useDropdownState, useFeedback, useFileUpload, useMessageQueue, usePromptState, useServerEvents, useSlashCommands, useStableTools, useStrings, useTheme, useToolSystem, validateCommandName };
package/dist/index.js CHANGED
@@ -56,7 +56,9 @@ var defaultStrings = {
56
56
  /** File size error (use {filename} and {maxSize} placeholders) */
57
57
  fileSizeError: 'File "{filename}" exceeds {maxSize}MB limit',
58
58
  /** File type error (use {type} placeholder) */
59
- fileTypeError: 'File type "{type}" is not accepted'
59
+ fileTypeError: 'File type "{type}" is not accepted',
60
+ /** Error when too many files are attached (use {max} placeholder) */
61
+ maxAttachmentsError: "You can attach at most {max} files per message"
60
62
  },
61
63
  // Floating button
62
64
  floatingButton: {
@@ -1405,6 +1407,9 @@ async function getTransformedContent(files, transformer, context, onProgress) {
1405
1407
  transformationCache.set(cacheKey, results);
1406
1408
  return results;
1407
1409
  }
1410
+ function normalizeUploadResult(result) {
1411
+ return typeof result === "string" ? { url: result } : result;
1412
+ }
1408
1413
  async function toContentPart(attachment, backend) {
1409
1414
  if (attachment.transformedContent !== void 0) {
1410
1415
  return {
@@ -1417,16 +1422,12 @@ async function toContentPart(attachment, backend) {
1417
1422
  }
1418
1423
  };
1419
1424
  }
1420
- const url = await backend.prepareForSend(attachment.file);
1421
- if (attachment.file.type.startsWith("image/")) {
1422
- return { type: "image", url };
1425
+ const result = normalizeUploadResult(await backend.prepareForSend(attachment.file));
1426
+ const isImage = attachment.file.type.startsWith("image/");
1427
+ if ("ref" in result) {
1428
+ return isImage ? { type: "image_ref", ref: result.ref } : { type: "file_ref", ref: result.ref, mimeType: attachment.file.type, name: attachment.file.name };
1423
1429
  }
1424
- return {
1425
- type: "file",
1426
- url,
1427
- mimeType: attachment.file.type,
1428
- name: attachment.file.name
1429
- };
1430
+ return isImage ? { type: "image_url", url: result.url } : { type: "file_url", url: result.url, mimeType: attachment.file.type, name: attachment.file.name };
1430
1431
  }
1431
1432
  async function processAttachments(attachments, config) {
1432
1433
  const { getCurrentChat, backend = new EmbedFileUploadBackend(), transformers = {}, onFileProgress } = config;
@@ -1529,7 +1530,12 @@ function useFileUpload({
1529
1530
  const enabled = config !== void 0;
1530
1531
  const maxFileSize = config?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
1531
1532
  const acceptedTypes = config?.acceptedTypes;
1533
+ const maxAttachments = config?.maxAttachments;
1532
1534
  const transformers = config?.transformers;
1535
+ const attachmentsRef = useRef3(attachments);
1536
+ useEffect3(() => {
1537
+ attachmentsRef.current = attachments;
1538
+ }, [attachments]);
1533
1539
  useEffect3(() => {
1534
1540
  if (fileError) {
1535
1541
  const timer = setTimeout(() => setFileError(null), 3e3);
@@ -1565,7 +1571,13 @@ function useFileUpload({
1565
1571
  }, [getCurrentChat, transformers]);
1566
1572
  const handleFiles = useCallback2(async (files) => {
1567
1573
  const fileArray = Array.from(files);
1574
+ let currentCount = attachmentsRef.current.length;
1568
1575
  for (const file of fileArray) {
1576
+ if (maxAttachments !== void 0 && currentCount >= maxAttachments) {
1577
+ const errorMsg = strings.fileUpload.maxAttachmentsError.replace("{max}", String(maxAttachments));
1578
+ setFileError(errorMsg);
1579
+ break;
1580
+ }
1569
1581
  if (file.size > maxFileSize) {
1570
1582
  const errorMsg = strings.fileUpload.fileSizeError.replace("{filename}", file.name).replace("{maxSize}", String(Math.round(maxFileSize / (1024 * 1024))));
1571
1583
  setFileError(errorMsg);
@@ -1584,14 +1596,17 @@ function useFileUpload({
1584
1596
  preview
1585
1597
  };
1586
1598
  setAttachments((prev) => [...prev, attachment]);
1599
+ attachmentsRef.current = [...attachmentsRef.current, attachment];
1600
+ currentCount++;
1587
1601
  const transformerKey = findTransformerPattern(file.type, transformers);
1588
1602
  if (transformerKey) {
1589
1603
  runTransformer(attachmentId, file, transformerKey);
1590
1604
  }
1591
1605
  }
1592
- }, [maxFileSize, acceptedTypes, strings, transformers, runTransformer]);
1606
+ }, [maxFileSize, acceptedTypes, maxAttachments, strings, transformers, runTransformer]);
1593
1607
  const removeAttachment = useCallback2((id) => {
1594
1608
  setAttachments((prev) => prev.filter((a) => a.id !== id));
1609
+ attachmentsRef.current = attachmentsRef.current.filter((a) => a.id !== id);
1595
1610
  setProcessingState((prev) => {
1596
1611
  const next = new Map(prev);
1597
1612
  next.delete(id);
@@ -1600,6 +1615,7 @@ function useFileUpload({
1600
1615
  }, []);
1601
1616
  const clearAttachments = useCallback2(() => {
1602
1617
  setAttachments([]);
1618
+ attachmentsRef.current = [];
1603
1619
  setProcessingState(/* @__PURE__ */ new Map());
1604
1620
  }, []);
1605
1621
  const openFilePicker = useCallback2(() => {
@@ -2170,7 +2186,16 @@ function FeedbackButton({ type, isSelected, onClick, selectedColor, unselectedCo
2170
2186
  );
2171
2187
  }
2172
2188
  function hasFileContent(content) {
2173
- return Array.isArray(content) && content.some((part) => part.type === "file");
2189
+ return Array.isArray(content) && content.some((part) => part.type === "file" || part.type === "attachment_ref");
2190
+ }
2191
+ function fileChipInfo(part) {
2192
+ if (part.type === "file") {
2193
+ return { name: part.file.name, size: part.file.size };
2194
+ }
2195
+ if (part.type === "attachment_ref") {
2196
+ return { name: part.name, size: part.size };
2197
+ }
2198
+ return null;
2174
2199
  }
2175
2200
  function UseAIChatPanel({
2176
2201
  onSendMessage,
@@ -2843,14 +2868,10 @@ function UseAIChatPanel({
2843
2868
  wordWrap: "break-word"
2844
2869
  },
2845
2870
  children: [
2846
- message.role === "user" && hasFileContent(message.content) && /* @__PURE__ */ jsx12("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "8px" }, children: message.content.filter((part) => part.type === "file").map((part, idx) => /* @__PURE__ */ jsx12(
2847
- FilePlaceholder,
2848
- {
2849
- name: part.file.name,
2850
- size: part.file.size
2851
- },
2852
- idx
2853
- )) }),
2871
+ message.role === "user" && hasFileContent(message.content) && /* @__PURE__ */ jsx12("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "8px" }, children: message.content.flatMap((part, idx) => {
2872
+ const info = fileChipInfo(part);
2873
+ return info ? [/* @__PURE__ */ jsx12(FilePlaceholder, { name: info.name, size: info.size }, idx)] : [];
2874
+ }) }),
2854
2875
  message.role === "assistant" ? /* @__PURE__ */ jsxs9(Fragment, { children: [
2855
2876
  message.reasoningParts && message.reasoningParts.length > 0 && /* @__PURE__ */ jsx12(
2856
2877
  Reasoning,
@@ -3669,31 +3690,13 @@ var UseAIClient = class {
3669
3690
  async sendPrompt(prompt, multimodalContent, forwardedProps) {
3670
3691
  let messageContent = prompt;
3671
3692
  if (multimodalContent && multimodalContent.length > 0) {
3672
- messageContent = multimodalContent.map((part) => {
3673
- if (part.type === "text") {
3674
- return { type: "text", text: part.text };
3675
- } else if (part.type === "image") {
3676
- return { type: "image", url: part.url };
3677
- } else if (part.type === "file") {
3678
- return {
3679
- type: "file",
3680
- url: part.url,
3681
- mimeType: part.mimeType
3682
- };
3683
- } else {
3684
- return {
3685
- type: "transformed_file",
3686
- text: part.text,
3687
- originalFile: part.originalFile
3688
- };
3689
- }
3690
- });
3693
+ messageContent = multimodalContent;
3691
3694
  }
3692
3695
  const userMessage = {
3693
3696
  id: uuidv42(),
3694
3697
  role: "user",
3698
+ // @ag-ui/core declares Message.content as `string`; the wire also carries MultimodalContent[].
3695
3699
  content: messageContent
3696
- // Type cast needed for Message type compatibility
3697
3700
  };
3698
3701
  this._messages.push(userMessage);
3699
3702
  const runId = uuidv42();
@@ -4391,6 +4394,7 @@ function buildPersistedParts(message, attachments, fileContent) {
4391
4394
  parts.push({ type: "text", text: message });
4392
4395
  }
4393
4396
  const transformedByKey = /* @__PURE__ */ new Map();
4397
+ const nonTransformedParts = [];
4394
4398
  for (const part of fileContent) {
4395
4399
  if (part.type === "transformed_file") {
4396
4400
  const key = `${part.originalFile.name}:${part.originalFile.size}:${part.originalFile.mimeType}`;
@@ -4400,6 +4404,8 @@ function buildPersistedParts(message, attachments, fileContent) {
4400
4404
  } else {
4401
4405
  transformedByKey.set(key, [part]);
4402
4406
  }
4407
+ } else {
4408
+ nonTransformedParts.push(part);
4403
4409
  }
4404
4410
  }
4405
4411
  for (const attachment of attachments) {
@@ -4411,6 +4417,18 @@ function buildPersistedParts(message, attachments, fileContent) {
4411
4417
  text: transformed.text,
4412
4418
  originalFile: transformed.originalFile
4413
4419
  });
4420
+ continue;
4421
+ }
4422
+ const part = nonTransformedParts.shift();
4423
+ const ref = part && (part.type === "image_ref" || part.type === "file_ref") ? part.ref : void 0;
4424
+ if (ref) {
4425
+ parts.push({
4426
+ type: "attachment_ref",
4427
+ ref,
4428
+ name: attachment.file.name,
4429
+ mimeType: attachment.file.type,
4430
+ size: attachment.file.size
4431
+ });
4414
4432
  } else {
4415
4433
  parts.push({
4416
4434
  type: "file",
@@ -4463,9 +4481,18 @@ function transformMessagesToClientFormat(persistedMessages) {
4463
4481
  ${p.text}`
4464
4482
  }];
4465
4483
  }
4484
+ if (p.type === "attachment_ref") {
4485
+ return [
4486
+ p.mimeType.startsWith("image/") ? { type: "image_ref", ref: p.ref } : { type: "file_ref", ref: p.ref, mimeType: p.mimeType, name: p.name }
4487
+ ];
4488
+ }
4466
4489
  return [];
4467
4490
  });
4468
- return { id: msg.id, role: "user", content: parts };
4491
+ return {
4492
+ id: msg.id,
4493
+ role: "user",
4494
+ content: parts
4495
+ };
4469
4496
  }
4470
4497
  }
4471
4498
  });