diff --git a/.changeset/hot-cows-flash.md b/.changeset/hot-cows-flash.md new file mode 100644 index 000000000..ce612864c --- /dev/null +++ b/.changeset/hot-cows-flash.md @@ -0,0 +1,5 @@ +--- +"@assistant-ui/react-ai-sdk": patch +--- + +feat: add AttachmentAdapter for AI SDK diff --git a/.changeset/mean-poets-pretend.md b/.changeset/mean-poets-pretend.md new file mode 100644 index 000000000..cbeccd95a --- /dev/null +++ b/.changeset/mean-poets-pretend.md @@ -0,0 +1,7 @@ +--- +"@assistant-ui/react-playground": patch +"@assistant-ui/react-ai-sdk": patch +"@assistant-ui/react": patch +--- + +fix: message copy handling for runtimes diff --git a/packages/react-ai-sdk/src/ui/use-assistant/useVercelUseAssistantRuntime.tsx b/packages/react-ai-sdk/src/ui/use-assistant/useVercelUseAssistantRuntime.tsx index 1d68d2fc2..6227ed0e8 100644 --- a/packages/react-ai-sdk/src/ui/use-assistant/useVercelUseAssistantRuntime.tsx +++ b/packages/react-ai-sdk/src/ui/use-assistant/useVercelUseAssistantRuntime.tsx @@ -4,6 +4,7 @@ import { useCachedChunkedMessages } from "../utils/useCachedChunkedMessages"; import { convertMessage } from "../utils/convertMessage"; import { useInputSync } from "../utils/useInputSync"; import { toCreateMessage } from "../utils/toCreateMessage"; +import { vercelAttachmentAdapter } from "../utils/vercelAttachmentAdapter"; export const useVercelUseAssistantRuntime = ( assistantHelpers: ReturnType, @@ -23,6 +24,9 @@ export const useVercelUseAssistantRuntime = ( assistantHelpers.setInput(""); }, convertMessage, + adapters: { + attachments: vercelAttachmentAdapter, + }, }); useInputSync(assistantHelpers, runtime); diff --git a/packages/react-ai-sdk/src/ui/use-chat/useVercelUseChatRuntime.tsx b/packages/react-ai-sdk/src/ui/use-chat/useVercelUseChatRuntime.tsx index 7f1bb9b07..3e2cf7835 100644 --- a/packages/react-ai-sdk/src/ui/use-chat/useVercelUseChatRuntime.tsx +++ b/packages/react-ai-sdk/src/ui/use-chat/useVercelUseChatRuntime.tsx @@ -5,6 +5,7 @@ import { useExternalStoreRuntime } from "@assistant-ui/react"; import { useInputSync } from "../utils/useInputSync"; import { sliceMessagesUntil } from "../utils/sliceMessagesUntil"; import { toCreateMessage } from "../utils/toCreateMessage"; +import { vercelAttachmentAdapter } from "../utils/vercelAttachmentAdapter"; export const useVercelUseChatRuntime = ( chatHelpers: ReturnType, @@ -44,6 +45,9 @@ export const useVercelUseChatRuntime = ( chatHelpers.setInput(""); }, convertMessage, + adapters: { + attachments: vercelAttachmentAdapter, + }, }); useInputSync(chatHelpers, runtime); diff --git a/packages/react-ai-sdk/src/ui/utils/vercelAttachmentAdapter.ts b/packages/react-ai-sdk/src/ui/utils/vercelAttachmentAdapter.ts new file mode 100644 index 000000000..d5f73dc03 --- /dev/null +++ b/packages/react-ai-sdk/src/ui/utils/vercelAttachmentAdapter.ts @@ -0,0 +1,20 @@ +import { AttachmentAdapter } from "@assistant-ui/react"; +import { generateId } from "ai"; + +export const vercelAttachmentAdapter: AttachmentAdapter = { + async add({ file }) { + return { + id: generateId(), + type: "file", + name: file.name, + file, + }; + }, + async send() { + // noop + return { content: [] }; + }, + async remove() { + // noop + }, +}; diff --git a/packages/react-playground/src/lib/playground-runtime.ts b/packages/react-playground/src/lib/playground-runtime.ts index 03d412afc..41d796dcf 100644 --- a/packages/react-playground/src/lib/playground-runtime.ts +++ b/packages/react-playground/src/lib/playground-runtime.ts @@ -89,7 +89,7 @@ const CAPABILITIES = Object.freeze({ edit: false, reload: false, cancel: true, - unstable_copy: false, + unstable_copy: true, speak: false, attachments: false, }); diff --git a/packages/react/src/runtimes/external-store/ExternalStoreAdapter.tsx b/packages/react/src/runtimes/external-store/ExternalStoreAdapter.tsx index 35ee01685..6e17ac353 100644 --- a/packages/react/src/runtimes/external-store/ExternalStoreAdapter.tsx +++ b/packages/react/src/runtimes/external-store/ExternalStoreAdapter.tsx @@ -1,5 +1,6 @@ import { AddToolResultOptions } from "../../context"; import { AppendMessage, ThreadMessage } from "../../types"; +import { AttachmentAdapter } from "../attachment"; import { SpeechSynthesisAdapter } from "../speech/SpeechAdapterTypes"; import { ThreadMessageLike } from "./ThreadMessageLike"; @@ -33,6 +34,9 @@ type ExternalStoreAdapterBase = { | ((message: ThreadMessage) => SpeechSynthesisAdapter.Utterance) | undefined; convertMessage?: ExternalStoreMessageConverter | undefined; + adapters?: { + attachments?: AttachmentAdapter | undefined; + }; unstable_capabilities?: | { copy?: boolean | undefined; diff --git a/packages/react/src/runtimes/external-store/ExternalStoreThreadRuntime.tsx b/packages/react/src/runtimes/external-store/ExternalStoreThreadRuntime.tsx index fdc4b328c..79d65ede4 100644 --- a/packages/react/src/runtimes/external-store/ExternalStoreThreadRuntime.tsx +++ b/packages/react/src/runtimes/external-store/ExternalStoreThreadRuntime.tsx @@ -75,11 +75,13 @@ export class ExternalStoreThreadRuntime implements ReactThreadRuntime { edit: this._store.onEdit !== undefined, reload: this._store.onReload !== undefined, cancel: this._store.onCancel !== undefined, - unstable_copy: this._store.unstable_capabilities?.copy !== null, speak: this._store.onSpeak !== undefined, - attachments: false, + unstable_copy: this._store.unstable_capabilities?.copy !== false, // default true + attachments: !!this.store.adapters?.attachments, }; + this.composer.attachmentAdapter = this._store.adapters?.attachments; + if (oldStore) { // flush the converter cache when the convertMessage prop changes if (oldStore.convertMessage !== store.convertMessage) { diff --git a/packages/react/src/runtimes/local/LocalThreadRuntime.tsx b/packages/react/src/runtimes/local/LocalThreadRuntime.tsx index 5e3088a73..a85bfe172 100644 --- a/packages/react/src/runtimes/local/LocalThreadRuntime.tsx +++ b/packages/react/src/runtimes/local/LocalThreadRuntime.tsx @@ -81,8 +81,8 @@ export class LocalThreadRuntime implements ThreadRuntime { hasUpdates = true; } - this.composer.adapter = options.adapters?.attachments; - const canAttach = this.composer.adapter !== undefined; + this.composer.attachmentAdapter = options.adapters?.attachments; + const canAttach = this.composer.attachmentAdapter !== undefined; if (this.capabilities.attachments !== canAttach) { this.capabilities.attachments = canAttach; hasUpdates = true; diff --git a/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx b/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx index 03bd0ddf8..0c452c61a 100644 --- a/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx +++ b/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx @@ -4,7 +4,7 @@ import { AttachmentAdapter } from "../attachment/AttachmentAdapter"; import { ThreadRuntime } from "../core"; export class ThreadRuntimeComposer implements ThreadRuntime.Composer { - public adapter?: AttachmentAdapter | undefined; + public attachmentAdapter?: AttachmentAdapter | undefined; constructor( private runtime: { @@ -21,22 +21,24 @@ export class ThreadRuntimeComposer implements ThreadRuntime.Composer { } async addAttachment(file: File) { - if (!this.adapter) throw new Error("Attachments are not supported"); + if (!this.attachmentAdapter) + throw new Error("Attachments are not supported"); - const attachment = await this.adapter.add({ file }); + const attachment = await this.attachmentAdapter.add({ file }); this._attachments = [...this._attachments, attachment]; this.notifySubscribers(); } async removeAttachment(attachmentId: string) { - if (!this.adapter) throw new Error("Attachments are not supported"); + if (!this.attachmentAdapter) + throw new Error("Attachments are not supported"); const index = this._attachments.findIndex((a) => a.id === attachmentId); if (index === -1) throw new Error("Attachment not found"); const attachment = this._attachments[index]!; - await this.adapter.remove(attachment); + await this.attachmentAdapter.remove(attachment); this._attachments = this._attachments.toSpliced(index, 1); this.notifySubscribers(); @@ -60,10 +62,10 @@ export class ThreadRuntimeComposer implements ThreadRuntime.Composer { } public async send() { - const attachmentContentParts = this.adapter + const attachmentContentParts = this.attachmentAdapter ? await Promise.all( this.attachments.map(async (a) => { - const { content } = await this.adapter!.send(a); + const { content } = await this.attachmentAdapter!.send(a); return content; }), )