Skip to content

Commit

Permalink
feat: ComposerAttachment and MessageAttachment (#777)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yonom authored Sep 8, 2024
1 parent b295e34 commit 600e9ab
Show file tree
Hide file tree
Showing 16 changed files with 135 additions and 68 deletions.
28 changes: 23 additions & 5 deletions apps/docs/components/docs/parameters/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,23 +525,41 @@ export const MessageUtilsState: ParametersTableProps = {
export const AttachmentContextValue: ParametersTableProps = {
type: "AttachmentContextValue",
parameters: [
{
name: "type",
type: "'composer' | 'message'",
required: true,
description: "The type of attachment.",
},
{
name: "useAttachment",
type: "ReadonlyStore<AttachmentState>",
type: "ReadonlyStore<ComposerAttachmentState | MessageAttachmentState>",
required: true,
description: "Provides functions to perform actions on the attachment.",
},
],
};

export const AttachmentState: ParametersTableProps = {
type: "AttachmentState",
export const ComposerAttachmentState: ParametersTableProps = {
type: "ComposerAttachmentState",
parameters: [
{
name: "attachment",
type: "ComposerAttachment",
required: true,
description: "The current composer attachment.",
},
],
};

export const MessageAttachmentState: ParametersTableProps = {
type: "MessageAttachmentState",
parameters: [
{
name: "attachment",
type: "Attachment",
type: "MessageAttachment",
required: true,
description: "The current attachment.",
description: "The current message attachment.",
},
],
};
11 changes: 9 additions & 2 deletions apps/docs/content/docs/reference/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
MessageState,
MessageUtilsState,
AttachmentContextValue,
AttachmentState,
ComposerAttachmentState,
MessageAttachmentState,
} from "@/components/docs/parameters/context";

## Assistant Context
Expand Down Expand Up @@ -380,4 +381,10 @@ const attachment = useAttachment((m) => m.attachment);
const attachment = useAttachment.getState().attachment;
```

<ParametersTable {...AttachmentState} />
#### `useAttachment` (Composer)

<ParametersTable {...ComposerAttachmentState} />

#### `useAttachment` (Message)

<ParametersTable {...MessageAttachmentState} />
8 changes: 6 additions & 2 deletions packages/react-ai-sdk/src/ui/utils/vercelAttachmentAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ export const vercelAttachmentAdapter: AttachmentAdapter = {
type: "file",
name: file.name,
file,
content: [],
};
},
async send() {
async send(attachment) {
// noop
return { content: [] };
return {
...attachment,
content: [],
};
},
async remove() {
// noop
Expand Down
24 changes: 12 additions & 12 deletions packages/react/src/context/providers/ComposerAttachmentProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"use client";

import { type FC, type PropsWithChildren, useEffect, useState } from "react";
import { StoreApi, create } from "zustand";
import { create } from "zustand";
import type { ComposerState } from "../stores";
import { useThreadContext } from "../react";
import { AttachmentState } from "../stores/Attachment";
import { ComposerAttachmentState } from "../stores/Attachment";
import {
AttachmentContext,
AttachmentContextValue,
} from "../react/AttachmentContext";
import { writableStore } from "../ReadonlyStore";

type ComposerAttachmentProviderProps = PropsWithChildren<{
attachmentIndex: number;
Expand All @@ -31,13 +32,15 @@ const getAttachment = (

const useComposerAttachmentContext = (partIndex: number) => {
const { useComposer } = useThreadContext();
const [context] = useState<AttachmentContextValue>(() => {
const useAttachment = create<AttachmentState>(
() => getAttachment(useComposer.getState(), undefined, partIndex)!,
);
const [context] = useState<AttachmentContextValue & { type: "composer" }>(
() => {
const useAttachment = create<ComposerAttachmentState>(
() => getAttachment(useComposer.getState(), undefined, partIndex)!,
);

return { useAttachment };
});
return { type: "composer", useAttachment };
},
);

useEffect(() => {
const syncAttachment = (composer: ComposerState) => {
Expand All @@ -47,10 +50,7 @@ const useComposerAttachmentContext = (partIndex: number) => {
partIndex,
);
if (!newState) return;
(context.useAttachment as unknown as StoreApi<AttachmentState>).setState(
newState,
true,
);
writableStore(context.useAttachment).setState(newState, true);
};

syncAttachment(useComposer.getState());
Expand Down
36 changes: 30 additions & 6 deletions packages/react/src/context/react/AttachmentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,49 @@

import { createContext, useContext } from "react";
import { ReadonlyStore } from "../ReadonlyStore";
import { AttachmentState } from "../stores/Attachment";
import {
ComposerAttachmentState,
MessageAttachmentState,
} from "../stores/Attachment";

export type AttachmentContextValue = {
useAttachment: ReadonlyStore<AttachmentState>;
};
export type AttachmentContextValue =
| {
type: "composer";
useAttachment: ReadonlyStore<ComposerAttachmentState>;
}
| {
type: "message";
useAttachment: ReadonlyStore<MessageAttachmentState>;
};

export const AttachmentContext = createContext<AttachmentContextValue | null>(
null,
);

export function useAttachmentContext(): AttachmentContextValue;
export function useAttachmentContext<
TType extends AttachmentContextValue["type"],
>(options: { type: TType }): AttachmentContextValue & { type: TType };
export function useAttachmentContext(options: {
optional: true;
}): AttachmentContextValue | null;
export function useAttachmentContext(options?: { optional: true }) {
export function useAttachmentContext(options?: {
type?: AttachmentContextValue["type"];
optional?: true;
}) {
const context = useContext(AttachmentContext);
if (!options?.optional && !context)
if (options?.type === "composer" && context?.type !== "composer")
throw new Error(
"This component must be used within a ComposerPrimitive.Attachments component.",
);
if (options?.type === "message" && context?.type !== "message")
throw new Error(
"This component must be used within a MessagePrimitive.Attachments component.",
);
if (!options?.optional && !context)
throw new Error(
"This component must be used within a ComposerPrimitive.Attachments or MessagePrimitive.Attachments component.",
);

return context;
}
19 changes: 16 additions & 3 deletions packages/react/src/context/stores/Attachment.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
export type Attachment = {
import { CoreUserContentPart } from "../../types";

export type BaseAttachment = {
id: string;
type: "image" | "document" | "file";
name: string;
};

export type ComposerAttachment = BaseAttachment & {
file: File;
};

export type MessageAttachment = BaseAttachment & {
file?: File;
content: CoreUserContentPart[];
};

export type AttachmentState = Readonly<{
attachment: Attachment;
export type ComposerAttachmentState = Readonly<{
attachment: ComposerAttachment;
}>;

export type MessageAttachmentState = Readonly<{
attachment: MessageAttachment;
}>;
4 changes: 2 additions & 2 deletions packages/react/src/context/stores/Composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { create } from "zustand";
import { ReadonlyStore } from "../ReadonlyStore";
import { Unsubscribe } from "../../types/Unsubscribe";
import { ThreadContextValue } from "../react";
import { Attachment } from "./Attachment";
import { ComposerAttachment } from "./Attachment";

export type ComposerState = Readonly<{
/** @deprecated Use `text` instead. */
value: string;
/** @deprecated Use `setText` instead. */
setValue: (value: string) => void;

attachments: readonly Attachment[];
attachments: readonly ComposerAttachment[];
addAttachment: (file: File) => void;
removeAttachment: (attachmentId: string) => void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Attachment } from "../../context/stores/Attachment";
import type { ComposerAttachment } from "../../context/stores/Attachment";

export type ComposerPrimitiveAttachmentsProps = {
components:
Expand All @@ -19,7 +19,7 @@ export type ComposerPrimitiveAttachmentsProps = {

const getComponent = (
components: ComposerPrimitiveAttachmentsProps["components"],
attachment: Attachment,
attachment: ComposerAttachment,
) => {
const type = attachment.type;
switch (type) {
Expand All @@ -38,7 +38,7 @@ const getComponent = (
const AttachmentComponent: FC<{
components: ComposerPrimitiveAttachmentsProps["components"];
}> = ({ components }) => {
const { useAttachment } = useAttachmentContext();
const { useAttachment } = useAttachmentContext({ type: "composer" });
const Component = useAttachment((a) =>
getComponent(components, a.attachment),
);
Expand Down
14 changes: 7 additions & 7 deletions packages/react/src/runtimes/attachment/AttachmentAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Attachment } from "../../context/stores/Attachment";
import { CoreUserContentPart } from "../../types";
import {
ComposerAttachment,
MessageAttachment,
} from "../../context/stores/Attachment";

export type AttachmentAdapter = {
add(state: { file: File }): Promise<Attachment>;
send(attachment: Attachment): Promise<{
content: CoreUserContentPart[];
}>;
remove(attachment: Attachment): Promise<void>;
add(state: { file: File }): Promise<ComposerAttachment>;
remove(attachment: ComposerAttachment): Promise<void>;
send(attachment: ComposerAttachment): Promise<MessageAttachment>;
};
4 changes: 2 additions & 2 deletions packages/react/src/runtimes/core/ThreadRuntime.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Attachment } from "../../context/stores/Attachment";
import { ComposerAttachment } from "../../context/stores/Attachment";
import { RuntimeCapabilities } from "../../context/stores/Thread";
import { ThreadActionsState } from "../../context/stores/ThreadActions";
import { ThreadMessage } from "../../types";
Expand All @@ -16,7 +16,7 @@ export type ThreadRuntime = ThreadActionsState &

export declare namespace ThreadRuntime {
export type Composer = Readonly<{
attachments: Attachment[];
attachments: ComposerAttachment[];
addAttachment: (file: File) => Promise<void>;
removeAttachment: (attachmentId: string) => Promise<void>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Attachment } from "../../../context/stores/Attachment";
import { generateId } from "../../../internal";
import {
ThreadMessage,
Expand All @@ -14,8 +13,7 @@ export const fromCoreMessages = (
};

export const fromCoreMessage = (
// TODO clean up this type
message: CoreMessage & { attachments?: readonly Attachment[] | undefined },
message: CoreMessage,
{
id = generateId(),
status = { type: "complete", reason: "unknown" } as MessageStatus,
Expand Down Expand Up @@ -49,7 +47,7 @@ export const fromCoreMessage = (
...commonProps,
role,
content: message.content,
attachments: message.attachments ?? [],
attachments: [],
} satisfies ThreadMessage;

case "system":
Expand Down
14 changes: 9 additions & 5 deletions packages/react/src/runtimes/edge/converters/toCoreMessages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ThreadMessage, CoreMessage } from "../../../types";
import { ThreadMessage, CoreMessage } from "../../../types";

export const toCoreMessages = (message: ThreadMessage[]): CoreMessage[] => {
return message.map(toCoreMessage);
Expand All @@ -23,10 +23,14 @@ export const toCoreMessage = (message: ThreadMessage): CoreMessage => {
case "user":
return {
role,
content: message.content.map((part) => {
if (part.type === "ui") throw new Error("UI parts are not supported");
return part;
}),
content: [
...message.content.map((part) => {
if (part.type === "ui")
throw new Error("UI parts are not supported");
return part;
}),
...message.attachments.map((a) => a.content).flat(),
],
};

case "system":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Attachment } from "../../context/stores/Attachment";
import { MessageAttachment } from "../../context/stores/Attachment";
import {
MessageStatus,
TextContentPart,
Expand Down Expand Up @@ -28,7 +28,7 @@ export type ThreadMessageLike = {
id?: string | undefined;
createdAt?: Date | undefined;
status?: MessageStatus | undefined;
attachments?: Attachment[] | undefined;
attachments?: MessageAttachment[] | undefined;
};

export const fromThreadMessageLike = (
Expand Down
Loading

0 comments on commit 600e9ab

Please sign in to comment.