Skip to content

Commit

Permalink
refactor: add BaseThreadRuntimeCore class, work towards edit composer…
Browse files Browse the repository at this point in the history
… attachment support (#970)
  • Loading branch information
Yonom authored Oct 11, 2024
1 parent a388f4a commit 899b963
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 309 deletions.
6 changes: 6 additions & 0 deletions .changeset/large-points-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@assistant-ui/react-playground": patch
"@assistant-ui/react": patch
---

refactor: add BaseThreadRuntimeCore class
5 changes: 5 additions & 0 deletions .changeset/neat-frogs-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react": patch
---

feat: work towards Edit Composer attachment support
5 changes: 5 additions & 0 deletions .changeset/ninety-bikes-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react": patch
---

refactor: remove composerState.attachmentAccept, add composerRuntime.getAttachmentAccept()
3 changes: 2 additions & 1 deletion packages/react-playground/src/lib/playground-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const CAPABILITIES = Object.freeze({
reload: false,
cancel: true,
unstable_copy: true,
speak: false,
speech: false,
attachments: false,
feedback: false,
});
Expand All @@ -112,6 +112,7 @@ export class PlaygroundThreadRuntimeCore implements INTERNAL.ThreadRuntimeCore {
public readonly extras = undefined;
public readonly suggestions: readonly ThreadSuggestion[] = [];
public readonly speech = null;
public readonly adapters = undefined;

private configProvider = new ProxyConfigProvider();

Expand Down
22 changes: 8 additions & 14 deletions packages/react/src/api/ComposerRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ type LegacyThreadComposerState = Readonly<{
/** @deprecated Use `useComposerRuntime().setText` instead. This will be removed in 0.6.0. */
setValue: (value: string) => void;

attachmentAccept: string;
attachments: readonly Attachment[];

/** @deprecated Use `useComposerRuntime().addAttachment` instead. This will be removed in 0.6.0. */
Expand Down Expand Up @@ -98,7 +97,6 @@ type LegacyThreadComposerState = Readonly<{

type BaseComposerState = {
text: string;
attachmentAccept: string;
attachments: readonly Attachment[];

canCancel: boolean;
Expand Down Expand Up @@ -137,7 +135,6 @@ const getThreadComposerState = (
isEmpty: runtime?.isEmpty ?? true,
text: runtime?.text ?? "",
attachments: runtime?.attachments ?? EMPTY_ARRAY,
attachmentAccept: runtime?.attachmentAccept ?? "*",

value: runtime?.text ?? "",
setValue: runtime?.setText.bind(runtime) ?? METHOD_NOT_SUPPORTED,
Expand Down Expand Up @@ -167,7 +164,6 @@ const getEditComposerState = (
isEmpty: runtime?.isEmpty ?? true,
text: runtime?.text ?? "",
attachments: runtime?.attachments ?? EMPTY_ARRAY,
attachmentAccept: runtime?.attachmentAccept ?? "*",

value: runtime?.text ?? "",
setValue: runtime?.setText.bind(runtime) ?? METHOD_NOT_SUPPORTED,
Expand All @@ -194,9 +190,6 @@ export type ComposerRuntime = {
/** @deprecated Use `getState().text` instead. This will be removed in 0.6.0. */
readonly text: string;

/** @deprecated Use `getState().attachmentAccept` instead. This will be removed in 0.6.0. */
readonly attachmentAccept: string;

/** @deprecated Use `getState().attachments` instead. This will be removed in 0.6.0. */
readonly attachments: readonly Attachment[];

Expand All @@ -205,6 +198,8 @@ export type ComposerRuntime = {

setText(text: string): void;
setValue(text: string): void;

getAttachmentAccept(): string;
addAttachment(file: File): Promise<void>;

/** @deprecated Use `getAttachmentById(id).removeAttachment()` instead. This will be removed in 0.6.0. */
Expand Down Expand Up @@ -254,13 +249,6 @@ export abstract class ComposerRuntimeImpl
return this.getState().text;
}

/**
* @deprecated Use `getState().attachmentAccept` instead. This will be removed in 0.6.0.
*/
public get attachmentAccept() {
return this.getState().attachmentAccept;
}

/**
* @deprecated Use `getState().attachments` instead. This will be removed in 0.6.0.
*/
Expand Down Expand Up @@ -327,6 +315,12 @@ export abstract class ComposerRuntimeImpl
return this._core.subscribe(callback);
}

public getAttachmentAccept(): string {
const core = this._core.getState();
if (!core) throw new Error("Composer is not available");
return core.getAttachmentAccept();
}

public abstract getAttachmentByIndex(idx: number): AttachmentRuntime;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import { useCallback } from "react";
import { useComposer } from "../../context";
import { useThreadComposerStore } from "../../context/react/ThreadContext";
import { useComposer, useComposerRuntime } from "../../context";

export const useComposerAddAttachment = () => {
const disabled = useComposer((c) => !c.isEditing);

const threadComposerStore = useThreadComposerStore();
const threadRuntimeStore = useThreadComposerStore();
const composerRuntime = useComposerRuntime();
const callback = useCallback(() => {
const { addAttachment } = threadComposerStore.getState();
const { attachmentAccept } = threadRuntimeStore.getState();

const input = document.createElement("input");
input.type = "file";

const attachmentAccept = composerRuntime.getAttachmentAccept();
if (attachmentAccept !== "*") {
input.accept = attachmentAccept;
}

input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addAttachment(file);
composerRuntime.addAttachment(file);
};

input.click();
}, [threadComposerStore, threadRuntimeStore]);
}, [composerRuntime]);

if (disabled) return null;
return callback;
Expand Down
56 changes: 26 additions & 30 deletions packages/react/src/runtimes/composer/BaseComposerRuntimeCore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ const isAttachmentComplete = (a: Attachment): a is CompleteAttachment =>
export abstract class BaseComposerRuntimeCore implements ComposerRuntimeCore {
public readonly isEditing = true;

public attachmentAccept: string = "*";
protected abstract getAttachmentAdapter(): AttachmentAdapter | undefined;

public getAttachmentAccept(): string {
return this.getAttachmentAdapter()?.accept ?? "*";
}

private _attachments: readonly Attachment[] = [];
protected set attachments(value: readonly Attachment[]) {
Expand Down Expand Up @@ -48,19 +52,21 @@ export abstract class BaseComposerRuntimeCore implements ComposerRuntimeCore {
}

public async send() {
const attachments = this._attachmentAdapter
? await Promise.all(
this.attachments.map(async (a) => {
if (isAttachmentComplete(a)) return a;
const result = await this._attachmentAdapter!.send(a);
// TODO remove after 0.6.0
if (result.status?.type !== "complete") {
result.status = { type: "complete" };
}
return result as CompleteAttachment;
}),
)
: [];
const adapter = this.getAttachmentAdapter();
const attachments =
adapter && this.attachments.length > 0
? await Promise.all(
this.attachments.map(async (a) => {
if (isAttachmentComplete(a)) return a;
const result = await adapter.send(a);
// TODO remove after 0.6.0
if (result.status?.type !== "complete") {
result.status = { type: "complete" };
}
return result as CompleteAttachment;
}),
)
: [];

const message: Omit<AppendMessage, "parentId"> = {
role: "user",
Expand All @@ -74,21 +80,11 @@ export abstract class BaseComposerRuntimeCore implements ComposerRuntimeCore {
public abstract handleSend(message: Omit<AppendMessage, "parentId">): void;
public abstract cancel(): void;

protected _attachmentAdapter?: AttachmentAdapter | undefined;
public setAttachmentAdapter(adapter: AttachmentAdapter | undefined) {
this._attachmentAdapter = adapter;
const accept = adapter?.accept ?? "*";
if (this.attachmentAccept !== accept) {
this.attachmentAccept = accept;
this.notifySubscribers();
}
}

async addAttachment(file: File) {
if (!this._attachmentAdapter)
throw new Error("Attachments are not supported");
const adapter = this.getAttachmentAdapter();
if (!adapter) throw new Error("Attachments are not supported");

const attachment = await this._attachmentAdapter.add({ file });
const attachment = await adapter.add({ file });
// TODO remove after 0.6.0
if (attachment.status === undefined) {
attachment.status = { type: "requires-action", reason: "composer-send" };
Expand All @@ -99,14 +95,14 @@ export abstract class BaseComposerRuntimeCore implements ComposerRuntimeCore {
}

async removeAttachment(attachmentId: string) {
if (!this._attachmentAdapter)
throw new Error("Attachments are not supported");
const adapter = this.getAttachmentAdapter();
if (!adapter) 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._attachmentAdapter.remove(attachment);
await adapter.remove(attachment);

this._attachments = this._attachments.toSpliced(index, 1);
this.notifySubscribers();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppendMessage, ThreadMessage } from "../../types";
import { getThreadMessageText } from "../../utils/getThreadMessageText";
import { AttachmentAdapter } from "../attachment";
import { ThreadRuntimeCore } from "../core/ThreadRuntimeCore";
import { BaseComposerRuntimeCore } from "./BaseComposerRuntimeCore";

Expand All @@ -8,11 +9,17 @@ export class DefaultEditComposerRuntimeCore extends BaseComposerRuntimeCore {
return true;
}

protected getAttachmentAdapter() {
return this.runtime.adapters?.attachments;
}

private _nonTextParts;
private _previousText;
private _parentId;
constructor(
private runtime: Omit<ThreadRuntimeCore, "composer">,
private runtime: Omit<ThreadRuntimeCore, "composer"> & {
adapters?: { attachments?: AttachmentAdapter | undefined } | undefined;
},
private endEditCallback: () => void,
{ parentId, message }: { parentId: string | null; message: ThreadMessage },
) {
Expand All @@ -25,8 +32,7 @@ export class DefaultEditComposerRuntimeCore extends BaseComposerRuntimeCore {
(part) => part.type !== "text" && part.type !== "ui",
);

// TODO differentiate between "sent" and "pending" attachments instead of Composer/Message Attachments
// this.attachments = message.attachments ?? [];
this.attachments = message.attachments ?? [];
}

public async handleSend(message: Omit<AppendMessage, "parentId">) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppendMessage, PendingAttachment } from "../../types";
import { AttachmentAdapter } from "../attachment";
import { ThreadComposerRuntimeCore } from "../core/ComposerRuntimeCore";
import { ThreadRuntimeCore } from "../core/ThreadRuntimeCore";
import { BaseComposerRuntimeCore } from "./BaseComposerRuntimeCore";
Expand All @@ -16,7 +17,15 @@ export class DefaultThreadComposerRuntimeCore
return super.attachments as readonly PendingAttachment[];
}

constructor(private runtime: Omit<ThreadRuntimeCore, "composer">) {
protected getAttachmentAdapter() {
return this.runtime.adapters?.attachments;
}

constructor(
private runtime: Omit<ThreadRuntimeCore, "composer"> & {
adapters?: { attachments?: AttachmentAdapter | undefined } | undefined;
},
) {
super();
this.connect();
}
Expand Down
Loading

0 comments on commit 899b963

Please sign in to comment.