Skip to content

Commit

Permalink
feat: ComposerAttachment UI (#786)
Browse files Browse the repository at this point in the history
* feat: ComposerAttachment UI

* rename ComposerAttachment type to ThreadComposerAttachment
  • Loading branch information
Yonom authored Sep 9, 2024
1 parent 3b0f20b commit 6d619b1
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 91 deletions.
9 changes: 7 additions & 2 deletions packages/react/src/context/react/AttachmentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import {
MessageAttachmentState,
} from "../stores/Attachment";

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

export const AttachmentContext = createContext<AttachmentContextValue | null>(
null,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/context/stores/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type BaseAttachment = {
name: string;
};

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

Expand All @@ -16,7 +16,7 @@ export type MessageAttachment = BaseAttachment & {
};

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

export type MessageAttachmentState = Readonly<{
Expand Down
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 { ComposerAttachment } from "./Attachment";
import { ThreadComposerAttachment } from "./Attachment";

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

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

Expand Down
14 changes: 7 additions & 7 deletions packages/react/src/primitives/composer/ComposerAttachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ 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:
| {
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}`);
Expand Down Expand Up @@ -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<
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/runtimes/attachment/AttachmentAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
ComposerAttachment,
ThreadComposerAttachment,
MessageAttachment,
} from "../../context/stores/Attachment";

export type AttachmentAdapter = {
accept: string;
add(state: { file: File }): Promise<ComposerAttachment>;
remove(attachment: ComposerAttachment): Promise<void>;
send(attachment: ComposerAttachment): Promise<MessageAttachment>;
add(state: { file: File }): Promise<ThreadComposerAttachment>;
remove(attachment: ThreadComposerAttachment): Promise<void>;
send(attachment: ThreadComposerAttachment): Promise<MessageAttachment>;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ComposerAttachment,
ThreadComposerAttachment,
MessageAttachment,
} from "../../context/stores/Attachment";
import { AttachmentAdapter } from "./AttachmentAdapter";
Expand Down Expand Up @@ -65,7 +65,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter {
}
}

public async add(state: { file: File }): Promise<ComposerAttachment> {
public async add(state: { file: File }): Promise<ThreadComposerAttachment> {
for (const adapter of this._adapters) {
if (fileMatchesAccept(state.file, adapter.accept)) {
return adapter.add(state);
Expand All @@ -75,7 +75,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter {
}

public async send(
attachment: ComposerAttachment,
attachment: ThreadComposerAttachment,
): Promise<MessageAttachment> {
const adapters = this._adapters.slice();
for (const adapter of adapters) {
Expand All @@ -86,7 +86,7 @@ export class ComposedAttachmentAdapter implements AttachmentAdapter {
throw new Error("No matching adapter found for attachment");
}

public async remove(attachment: ComposerAttachment): Promise<void> {
public async remove(attachment: ThreadComposerAttachment): Promise<void> {
const adapters = this._adapters.slice();
for (const adapter of adapters) {
if (fileMatchesAccept(attachment.file, adapter.accept)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
ComposerAttachment,
ThreadComposerAttachment,
MessageAttachment,
} from "../../context/stores/Attachment";
import { AttachmentAdapter } from "./AttachmentAdapter";

export class SimpleImageAttachmentAdapter implements AttachmentAdapter {
public accept = "image/*";

public async add(state: { file: File }): Promise<ComposerAttachment> {
public async add(state: { file: File }): Promise<ThreadComposerAttachment> {
return {
id: state.file.name,
type: "image",
Expand All @@ -17,7 +17,7 @@ export class SimpleImageAttachmentAdapter implements AttachmentAdapter {
}

public async send(
attachment: ComposerAttachment,
attachment: ThreadComposerAttachment,
): Promise<MessageAttachment> {
return {
...attachment,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ComposerAttachment,
ThreadComposerAttachment,
MessageAttachment,
} from "../../context/stores/Attachment";
import { AttachmentAdapter } from "./AttachmentAdapter";
Expand All @@ -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<ComposerAttachment> {
public async add(state: { file: File }): Promise<ThreadComposerAttachment> {
return {
id: state.file.name,
type: "document",
Expand All @@ -18,7 +18,7 @@ export class SimpleTextAttachmentAdapter implements AttachmentAdapter {
}

public async send(
attachment: ComposerAttachment,
attachment: ThreadComposerAttachment,
): Promise<MessageAttachment> {
return {
...attachment,
Expand Down
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 { 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";
Expand All @@ -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<void>;
removeAttachment: (attachmentId: string) => Promise<void>;

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/runtimes/utils/ThreadRuntimeComposer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -30,7 +30,7 @@ export class ThreadRuntimeComposer implements ThreadRuntime.Composer {
return false;
}

private _attachments: ComposerAttachment[] = [];
private _attachments: ThreadComposerAttachment[] = [];

public get attachments() {
return this._attachments;
Expand Down
77 changes: 77 additions & 0 deletions packages/react/src/ui/composer-attachment.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ComposerAttachmentRoot>
.{attachment.name.split(".").pop()}
<ComposerAttachmentRemove />
</ComposerAttachmentRoot>
);
};

ComposerAttachment.displayName = "ComposerAttachment";

const ComposerAttachmentRemove = forwardRef<
HTMLButtonElement,
Partial<TooltipIconButtonProps>
>((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 (
<TooltipIconButton
tooltip={tooltip}
className="aui-composer-attachment-remove"
side="top"
{...props}
onClick={handleRemoveAttachment}
ref={ref}
>
{props.children ?? <CircleXIcon />}
</TooltipIconButton>
);
});

ComposerAttachmentRemove.displayName = "ComposerAttachmentRemove";

const exports = {
Root: ComposerAttachmentRoot,
Remove: ComposerAttachmentRemove,
};

export default Object.assign(
ComposerAttachment,
exports,
) as typeof ComposerAttachment & typeof exports;
Loading

0 comments on commit 6d619b1

Please sign in to comment.