diff --git a/packages/react/src/context/react/AttachmentContext.ts b/packages/react/src/context/react/AttachmentContext.ts index c79df8a9bc..269b40277d 100644 --- a/packages/react/src/context/react/AttachmentContext.ts +++ b/packages/react/src/context/react/AttachmentContext.ts @@ -7,7 +7,11 @@ import { MessageAttachmentState, } from "../stores/Attachment"; -export type AttachmentContextValue = +export type AttachmentContextValue = { + useAttachment: ReadonlyStore< + ComposerAttachmentState | MessageAttachmentState + >; +} & ( | { type: "composer"; useAttachment: ReadonlyStore; @@ -15,7 +19,8 @@ export type AttachmentContextValue = | { type: "message"; useAttachment: ReadonlyStore; - }; + } +); export const AttachmentContext = createContext( null, diff --git a/packages/react/src/context/stores/Attachment.ts b/packages/react/src/context/stores/Attachment.ts index 9c424b4a32..014ac3e2b8 100644 --- a/packages/react/src/context/stores/Attachment.ts +++ b/packages/react/src/context/stores/Attachment.ts @@ -6,7 +6,7 @@ export type BaseAttachment = { name: string; }; -export type ComposerAttachment = BaseAttachment & { +export type ThreadComposerAttachment = BaseAttachment & { file: File; }; @@ -16,7 +16,7 @@ export type MessageAttachment = BaseAttachment & { }; export type ComposerAttachmentState = Readonly<{ - attachment: ComposerAttachment; + attachment: ThreadComposerAttachment; }>; export type MessageAttachmentState = Readonly<{ diff --git a/packages/react/src/context/stores/Composer.ts b/packages/react/src/context/stores/Composer.ts index def3a88d6a..fd2d3bc171 100644 --- a/packages/react/src/context/stores/Composer.ts +++ b/packages/react/src/context/stores/Composer.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { ReadonlyStore } from "../ReadonlyStore"; import { Unsubscribe } from "../../types/Unsubscribe"; import { ThreadContextValue } from "../react"; -import { ComposerAttachment } from "./Attachment"; +import { ThreadComposerAttachment } from "./Attachment"; export type ComposerState = Readonly<{ /** @deprecated Use `text` instead. */ @@ -10,7 +10,7 @@ export type ComposerState = Readonly<{ /** @deprecated Use `setText` instead. */ setValue: (value: string) => void; - attachments: readonly ComposerAttachment[]; + attachments: readonly ThreadComposerAttachment[]; addAttachment: (file: File) => void; removeAttachment: (attachmentId: string) => void; diff --git a/packages/react/src/primitives/composer/ComposerAttachments.tsx b/packages/react/src/primitives/composer/ComposerAttachments.tsx index 9ab47a5bd8..243f8334d8 100644 --- a/packages/react/src/primitives/composer/ComposerAttachments.tsx +++ b/packages/react/src/primitives/composer/ComposerAttachments.tsx @@ -4,7 +4,7 @@ import { ComponentType, type FC, memo } from "react"; import { useThreadContext } from "../../context"; import { useAttachmentContext } from "../../context/react/AttachmentContext"; import { ComposerAttachmentProvider } from "../../context/providers/ComposerAttachmentProvider"; -import type { ComposerAttachment } from "../../context/stores/Attachment"; +import type { ThreadComposerAttachment } from "../../context/stores/Attachment"; export type ComposerPrimitiveAttachmentsProps = { components: @@ -12,23 +12,23 @@ export type ComposerPrimitiveAttachmentsProps = { Image?: ComponentType | undefined; Document?: ComponentType | undefined; File?: ComponentType | undefined; - Fallback?: ComponentType | undefined; + Attachment?: ComponentType | undefined; } | undefined; }; const getComponent = ( components: ComposerPrimitiveAttachmentsProps["components"], - attachment: ComposerAttachment, + attachment: ThreadComposerAttachment, ) => { const type = attachment.type; switch (type) { case "image": - return components?.Image ?? components?.Fallback; + return components?.Image ?? components?.Attachment; case "document": - return components?.Document ?? components?.Fallback; + return components?.Document ?? components?.Attachment; case "file": - return components?.File ?? components?.Fallback; + return components?.File ?? components?.Attachment; default: const _exhaustiveCheck: never = type; throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`); @@ -64,7 +64,7 @@ const ComposerAttachment = memo( prev.components?.Image === next.components?.Image && prev.components?.Document === next.components?.Document && prev.components?.File === next.components?.File && - prev.components?.Fallback === next.components?.Fallback, + prev.components?.Attachment === next.components?.Attachment, ); export const ComposerPrimitiveAttachments: FC< diff --git a/packages/react/src/runtimes/attachment/AttachmentAdapter.ts b/packages/react/src/runtimes/attachment/AttachmentAdapter.ts index 0eabccf575..4c1a931c33 100644 --- a/packages/react/src/runtimes/attachment/AttachmentAdapter.ts +++ b/packages/react/src/runtimes/attachment/AttachmentAdapter.ts @@ -1,11 +1,11 @@ import { - ComposerAttachment, + ThreadComposerAttachment, MessageAttachment, } from "../../context/stores/Attachment"; export type AttachmentAdapter = { accept: string; - add(state: { file: File }): Promise; - remove(attachment: ComposerAttachment): Promise; - send(attachment: ComposerAttachment): Promise; + add(state: { file: File }): Promise; + remove(attachment: ThreadComposerAttachment): Promise; + send(attachment: ThreadComposerAttachment): Promise; }; diff --git a/packages/react/src/runtimes/attachment/ComposedAttachmentAdapter.ts b/packages/react/src/runtimes/attachment/ComposedAttachmentAdapter.ts index 2ba1864450..7b43c2dc8a 100644 --- a/packages/react/src/runtimes/attachment/ComposedAttachmentAdapter.ts +++ b/packages/react/src/runtimes/attachment/ComposedAttachmentAdapter.ts @@ -1,5 +1,5 @@ import { - ComposerAttachment, + ThreadComposerAttachment, MessageAttachment, } from "../../context/stores/Attachment"; import { AttachmentAdapter } from "./AttachmentAdapter"; @@ -65,7 +65,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter { } } - public async add(state: { file: File }): Promise { + public async add(state: { file: File }): Promise { for (const adapter of this._adapters) { if (fileMatchesAccept(state.file, adapter.accept)) { return adapter.add(state); @@ -75,7 +75,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter { } public async send( - attachment: ComposerAttachment, + attachment: ThreadComposerAttachment, ): Promise { const adapters = this._adapters.slice(); for (const adapter of adapters) { @@ -86,7 +86,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter { throw new Error("No matching adapter found for attachment"); } - public async remove(attachment: ComposerAttachment): Promise { + public async remove(attachment: ThreadComposerAttachment): Promise { const adapters = this._adapters.slice(); for (const adapter of adapters) { if (fileMatchesAccept(attachment.file, adapter.accept)) { diff --git a/packages/react/src/runtimes/attachment/SimpleImageAttachmentAdapter.ts b/packages/react/src/runtimes/attachment/SimpleImageAttachmentAdapter.ts index bb8eb612be..558dee2159 100644 --- a/packages/react/src/runtimes/attachment/SimpleImageAttachmentAdapter.ts +++ b/packages/react/src/runtimes/attachment/SimpleImageAttachmentAdapter.ts @@ -1,5 +1,5 @@ import { - ComposerAttachment, + ThreadComposerAttachment, MessageAttachment, } from "../../context/stores/Attachment"; import { AttachmentAdapter } from "./AttachmentAdapter"; @@ -7,7 +7,7 @@ import { AttachmentAdapter } from "./AttachmentAdapter"; export class SimpleImageAttachmentAdapter implements AttachmentAdapter { public accept = "image/*"; - public async add(state: { file: File }): Promise { + public async add(state: { file: File }): Promise { return { id: state.file.name, type: "image", @@ -17,7 +17,7 @@ export class SimpleImageAttachmentAdapter implements AttachmentAdapter { } public async send( - attachment: ComposerAttachment, + attachment: ThreadComposerAttachment, ): Promise { return { ...attachment, diff --git a/packages/react/src/runtimes/attachment/SimpleTextAttachmentAdapter.ts b/packages/react/src/runtimes/attachment/SimpleTextAttachmentAdapter.ts index a80190116e..e486f396fd 100644 --- a/packages/react/src/runtimes/attachment/SimpleTextAttachmentAdapter.ts +++ b/packages/react/src/runtimes/attachment/SimpleTextAttachmentAdapter.ts @@ -1,5 +1,5 @@ import { - ComposerAttachment, + ThreadComposerAttachment, MessageAttachment, } from "../../context/stores/Attachment"; import { AttachmentAdapter } from "./AttachmentAdapter"; @@ -8,7 +8,7 @@ export class SimpleTextAttachmentAdapter implements AttachmentAdapter { public accept = "text/plain,text/html,text/markdown,text/csv,text/xml,text/json,text/css"; - public async add(state: { file: File }): Promise { + public async add(state: { file: File }): Promise { return { id: state.file.name, type: "document", @@ -18,7 +18,7 @@ export class SimpleTextAttachmentAdapter implements AttachmentAdapter { } public async send( - attachment: ComposerAttachment, + attachment: ThreadComposerAttachment, ): Promise { return { ...attachment, diff --git a/packages/react/src/runtimes/core/ThreadRuntime.tsx b/packages/react/src/runtimes/core/ThreadRuntime.tsx index 0c4322c462..e09c6aee6c 100644 --- a/packages/react/src/runtimes/core/ThreadRuntime.tsx +++ b/packages/react/src/runtimes/core/ThreadRuntime.tsx @@ -1,4 +1,4 @@ -import { ComposerAttachment } from "../../context/stores/Attachment"; +import { ThreadComposerAttachment } from "../../context/stores/Attachment"; import { RuntimeCapabilities } from "../../context/stores/Thread"; import { ThreadActionsState } from "../../context/stores/ThreadActions"; import { ThreadMessage } from "../../types"; @@ -17,7 +17,7 @@ export type ThreadRuntime = ThreadActionsState & export declare namespace ThreadRuntime { export type Composer = Readonly<{ attachmentAccept: string; - attachments: ComposerAttachment[]; + attachments: ThreadComposerAttachment[]; addAttachment: (file: File) => Promise; removeAttachment: (attachmentId: string) => Promise; diff --git a/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx b/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx index ef65f676c9..5e8886a738 100644 --- a/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx +++ b/packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx @@ -1,4 +1,4 @@ -import { ComposerAttachment } from "../../context/stores/Attachment"; +import { ThreadComposerAttachment } from "../../context/stores/Attachment"; import { AppendMessage } from "../../types"; import { AttachmentAdapter } from "../attachment/AttachmentAdapter"; import { ThreadRuntime } from "../core"; @@ -30,7 +30,7 @@ export class ThreadRuntimeComposer implements ThreadRuntime.Composer { return false; } - private _attachments: ComposerAttachment[] = []; + private _attachments: ThreadComposerAttachment[] = []; public get attachments() { return this._attachments; diff --git a/packages/react/src/ui/composer-attachment.tsx b/packages/react/src/ui/composer-attachment.tsx new file mode 100644 index 0000000000..d41d41096d --- /dev/null +++ b/packages/react/src/ui/composer-attachment.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { forwardRef, type FC } from "react"; + +import { CircleXIcon } from "lucide-react"; +import { withDefaults } from "./utils/withDefaults"; +import { useThreadConfig } from "./thread-config"; +import { + TooltipIconButton, + TooltipIconButtonProps, +} from "./base/tooltip-icon-button"; +import { useThreadContext } from "../context/react/ThreadContext"; +import { useAttachmentContext } from "../context/react/AttachmentContext"; + +const ComposerAttachmentRoot = withDefaults("div", { + className: "aui-composer-attachment-root", +}); + +ComposerAttachmentRoot.displayName = "ComposerAttachmentRoot"; + +const ComposerAttachment: FC = () => { + const { useAttachment } = useAttachmentContext({ type: "composer" }); + const attachment = useAttachment((a) => a.attachment); + + return ( + + .{attachment.name.split(".").pop()} + + + ); +}; + +ComposerAttachment.displayName = "ComposerAttachment"; + +const ComposerAttachmentRemove = forwardRef< + HTMLButtonElement, + Partial +>((props, ref) => { + const { + strings: { + composer: { removeAttachment: { tooltip = "Remove file" } = {} } = {}, + } = {}, + } = useThreadConfig(); + + const { useComposer } = useThreadContext(); + const { useAttachment } = useAttachmentContext(); + const handleRemoveAttachment = () => { + useComposer + .getState() + .removeAttachment(useAttachment.getState().attachment.id); + }; + + return ( + + {props.children ?? } + + ); +}); + +ComposerAttachmentRemove.displayName = "ComposerAttachmentRemove"; + +const exports = { + Root: ComposerAttachmentRoot, + Remove: ComposerAttachmentRemove, +}; + +export default Object.assign( + ComposerAttachment, + exports, +) as typeof ComposerAttachment & typeof exports; diff --git a/packages/react/src/ui/composer.tsx b/packages/react/src/ui/composer.tsx index 3634930d93..3de07c8120 100644 --- a/packages/react/src/ui/composer.tsx +++ b/packages/react/src/ui/composer.tsx @@ -2,7 +2,7 @@ import { ComponentPropsWithoutRef, forwardRef, type FC } from "react"; -import { CircleXIcon, PaperclipIcon, SendHorizontalIcon } from "lucide-react"; +import { PaperclipIcon, SendHorizontalIcon } from "lucide-react"; import { withDefaults } from "./utils/withDefaults"; import { useThreadConfig } from "./thread-config"; import { @@ -12,7 +12,7 @@ import { import { CircleStopIcon } from "./base/CircleStopIcon"; import { ComposerPrimitive, ThreadPrimitive } from "../primitives"; import { useThreadContext } from "../context/react/ThreadContext"; -import { useAttachmentContext } from "../context/react/AttachmentContext"; +import ComposerAttachment from "./composer-attachment"; const useAllowAttachments = (ensureCapability = false) => { const { composer: { allowAttachments = true } = {} } = useThreadConfig(); @@ -63,74 +63,22 @@ const ComposerInput = forwardRef( }, ); +ComposerInput.displayName = "ComposerInput"; + const ComposerAttachmentsContainer = withDefaults("div", { - className: "aui-composer-attachments-container", + className: "aui-composer-attachments", }); const ComposerAttachments: FC = () => { return ( ); }; -const ComposerAttachmentContainer = withDefaults("div", { - className: "aui-composer-attachment-container", -}); - -const ComposerAttachment: FC = () => { - const { useAttachment } = useAttachmentContext({ type: "composer" }); - const attachment = useAttachment((a) => a.attachment); - - return ( - - .{attachment.name.split(".").pop()} - - - ); -}; - -ComposerAttachment.displayName = "ComposerAttachment"; - -const ComposerRemoveAttachment = forwardRef< - HTMLButtonElement, - Partial ->((props, ref) => { - const { - strings: { - composer: { removeAttachment: { tooltip = "Remove file" } = {} } = {}, - } = {}, - } = useThreadConfig(); - - const { useComposer } = useThreadContext(); - const { useAttachment } = useAttachmentContext(); - const handleRemoveAttachment = () => { - useComposer - .getState() - .removeAttachment(useAttachment.getState().attachment.id); - }; - - return ( - - {props.children ?? } - - ); -}); - -ComposerRemoveAttachment.displayName = "ComposerRemoveAttachment"; - -ComposerInput.displayName = "ComposerInput"; - const ComposerAttachButton = withDefaults(TooltipIconButton, { variant: "default", className: "aui-composer-attach", @@ -239,8 +187,6 @@ const exports = { Cancel: ComposerCancel, AddAttachment: ComposerAddAttachment, Attachments: ComposerAttachments, - Attachment: ComposerAttachment, - RemoveAttachment: ComposerRemoveAttachment, }; export default Object.assign(Composer, exports) as typeof Composer & diff --git a/packages/react/src/ui/index.ts b/packages/react/src/ui/index.ts index 3fad797b66..7631a285b8 100644 --- a/packages/react/src/ui/index.ts +++ b/packages/react/src/ui/index.ts @@ -23,6 +23,8 @@ export { default as BranchPicker } from "./branch-picker"; export { default as Composer, type ComposerInputProps } from "./composer"; +export { default as ComposerAttachment } from "./composer-attachment"; + export { default as EditComposer } from "./edit-composer"; export { default as Thread, type ThreadRootProps } from "./thread";