Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/packages/react-core/src/hooks/useThreadActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export const useThreadActions = (): ThreadActions => {
setMessages: useStore(threadManager, (store) => store.setMessages),
onCancel: useStore(threadManager, (store) => store.onCancel),
deleteMessage: useStore(threadManager, (store) => store.deleteMessage),
processFileUpload: useStore(threadManager, (store) => store.processFileUpload),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const useThreadManagerStore = (inputThreadManager: ThreadManager) => {
appendMessages: (...props) => inputThreadManagerRef.current.appendMessages(...props),
setMessages: (...props) => inputThreadManagerRef.current.setMessages(...props),
deleteMessage: (...props) => inputThreadManagerRef.current.deleteMessage(...props),
processFileUpload: (...props) => inputThreadManagerRef.current.processFileUpload(...props),
})),
);

Expand Down
4 changes: 3 additions & 1 deletion js/packages/react-core/src/types/chatManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreateMessage, Message, UserMessage } from "./message";
import { CreateMessage, Message, UploadedFile, UserMessage } from "./message";
import { ResponseTemplate } from "./responseTemplate";

/**
Expand Down Expand Up @@ -37,6 +37,8 @@ export type ThreadActions = {
setMessages: (messages: Message[]) => void;
/** Deletes a message from the thread */
deleteMessage: (messageId: string) => void;
/** Processes a file upload */
processFileUpload: (files: File[]) => Promise<UploadedFile[]>;
};

/**
Expand Down
12 changes: 12 additions & 0 deletions js/packages/react-core/src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ type Common = {
isVisuallyHidden?: boolean;
};

/**
* @inline
*/
export type UploadedFile = {
/** The name of the file */
name: string;
/** The id of the file */
fileId: string;
};

/**
* A type that represents a message sent by the user
*
Expand All @@ -23,6 +33,8 @@ export type UserMessage = Common & {
message?: string;
/** Additional data associated with the message */
context?: JSONValue[];
/** The files that are associated with the message */
files?: UploadedFile[];
};

/**
Expand Down
13 changes: 13 additions & 0 deletions js/packages/react-core/src/useThreadManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef } from "react";
import { createStore, useStore } from "zustand";
import { CreateMessage, Message, ResponseTemplate, ThreadManager } from "./types";
import { UploadedFile } from "./types/message";

/**
* Parameters to be passed to the {@link useThreadManager} hook
Expand All @@ -19,10 +20,16 @@ export type UseThreadManagerParams = {
message: CreateMessage;
threadManager: ThreadManager;
abortController: AbortController;
filesUploaded?: UploadedFile[];
}) => Promise<Message[]>;
/** A function that defines how a message should be updated. Useful for integrating a backend API to update a message. */
onUpdateMessage?: (props: { message: Message }) => void;
/** A list of response templates available to the thread. */
/** A function that defines how files should be uploaded. Useful for integrating a backend API to upload files. */
onProcessFileUpload?: (props: {
files: File[];
abortController?: AbortController;
}) => Promise<UploadedFile[]>;
responseTemplates: ResponseTemplate[];
};

Expand Down Expand Up @@ -54,6 +61,7 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
abortController: null,
isRunning: false,
isLoadingMessages: false,
isUploading: false,
setMessages: (messages: Message[]) => {
set({ messages });
},
Expand All @@ -77,6 +85,7 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
message,
threadManager: store.getState(),
abortController,
filesUploaded: message.files,
});

store.getState().appendMessages(...newMessages);
Expand Down Expand Up @@ -108,6 +117,10 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
const messages = store.getState().messages.filter((m) => m.id !== messageId);
set({ messages });
},
processFileUpload: async (files: File[]) => {
const response = await propsRef.current.onProcessFileUpload?.({ files });
return response ?? [];
},
responseTemplates: propsRef.current.responseTemplates.reduce(
(acc, template) => {
acc[template.name] = template;
Expand Down
43 changes: 39 additions & 4 deletions js/packages/react-ui/src/components/CopilotShell/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useThreadState,
} from "@crayonai/react-core";
import clsx from "clsx";
import { ArrowRight, Square } from "lucide-react";
import { ArrowRight, Plus, Square } from "lucide-react";
import React, { memo, useLayoutEffect, useRef } from "react";
import { useComposerState } from "../../hooks/useComposerState";
import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom";
Expand Down Expand Up @@ -193,11 +193,30 @@ export const Messages = ({
);
};

export const Composer = ({ className }: { className?: string }) => {
const { textContent, setTextContent } = useComposerState();
const { processMessage, onCancel } = useThreadActions();
export const Composer = ({
className,
enableFileUpload = false,
}: {
className?: string;
enableFileUpload?: boolean;
}) => {
const { textContent, setTextContent, uploadedFiles, setUploadedFiles } = useComposerState();
const { processMessage, onCancel, processFileUpload } = useThreadActions();
const { isRunning } = useThreadState();
const inputRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);

const handleUpload = () => {
fileInputRef.current?.click();
};

const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
const result = await processFileUpload(files);
setUploadedFiles({ files: result });
}
};

const handleSubmit = () => {
if (!textContent.trim() || isRunning) {
Expand All @@ -208,6 +227,7 @@ export const Composer = ({ className }: { className?: string }) => {
type: "prompt",
role: "user",
message: textContent,
files: uploadedFiles?.files ?? [],
});

setTextContent("");
Expand Down Expand Up @@ -239,6 +259,21 @@ export const Composer = ({ className }: { className?: string }) => {
}
}}
/>
{enableFileUpload && (
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
/>
)}
{(!isRunning || !!handleUpload) && enableFileUpload && (
<IconButton
variant="secondary"
onClick={isRunning ? onCancel : handleUpload}
icon={<Plus size="1em" />}
/>
)}
<IconButton
onClick={isRunning ? onCancel : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowRight size="1em" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ interface ComposedCopilotProps {
agentName?: string;
messageLoadingComponent?: () => React.ReactNode;
scrollVariant: ScrollVariant;
enableFileUpload: boolean;
}

export const ComposedCopilot = ({
logoUrl = "https://crayonai.org/img/logo.png",
agentName = "My Agent",
messageLoadingComponent: MessageLoadingComponent = MessageLoading,
scrollVariant,
enableFileUpload,
}: ComposedCopilotProps) => {
return (
<Container logoUrl={logoUrl} agentName={agentName}>
Expand All @@ -28,7 +30,7 @@ export const ComposedCopilot = ({
<ScrollArea scrollVariant={scrollVariant}>
<Messages loader={<MessageLoadingComponent />} />
</ScrollArea>
<Composer />
<Composer enableFileUpload={enableFileUpload} />
</ThreadContainer>
</Container>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ interface ComposedStandaloneProps {
agentName?: string;
messageLoadingComponent?: () => React.ReactNode;
scrollVariant: ScrollVariant;
enableFileUpload: boolean;
}

export const ComposedStandalone = ({
logoUrl = "https://crayonai.org/img/logo.png",
agentName = "My Agent",
messageLoadingComponent: MessageLoadingComponent = MessageLoading,
scrollVariant,
enableFileUpload,
}: ComposedStandaloneProps) => {
return (
<Container logoUrl={logoUrl} agentName={agentName}>
Expand All @@ -42,7 +44,7 @@ export const ComposedStandalone = ({
<ScrollArea scrollVariant={scrollVariant}>
<Messages loader={<MessageLoadingComponent />} />
</ScrollArea>
<Composer />
<Composer enableFileUpload={enableFileUpload} />
</ThreadContainer>
</Container>
);
Expand Down
15 changes: 14 additions & 1 deletion js/packages/react-ui/src/components/CrayonChat/CrayonChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Thread,
ThreadListManager,
ThreadManager,
UploadedFile,
UserMessage,
useThreadListManager,
useThreadManager,
Expand All @@ -25,6 +26,7 @@ type CrayonChatProps = {
messages: Message[];
abortController: AbortController;
}) => Promise<Response>;
processFileUpload?: (params: { files: File[] }) => Promise<UploadedFile[]>;
onUpdateMessage?: (props: { message: Message }) => void;
processStreamedMessage?: typeof processStreamedMessage;
responseTemplates?: ResponseTemplate[];
Expand Down Expand Up @@ -52,6 +54,7 @@ const DummyThemeProvider = ({ children }: { children: React.ReactNode }) => {

export const CrayonChat = ({
processMessage,
processFileUpload,
threadManager: userThreadManager,
threadListManager: userThreadListManager,
logoUrl = "https://crayonai.org/img/logo.png",
Expand Down Expand Up @@ -98,9 +101,10 @@ export const CrayonChat = ({
return Promise.resolve(messages);
},
onUpdateMessage: onUpdateMessage,
onProcessMessage: async ({ message, abortController, threadManager }) => {
onProcessMessage: async ({ message, abortController, threadManager, filesUploaded }) => {
const newMessage: UserMessage = {
id: crypto.randomUUID(),
files: filesUploaded,
...message,
};
threadManager.appendMessages(newMessage);
Expand Down Expand Up @@ -128,10 +132,17 @@ export const CrayonChat = ({

return [];
},
onProcessFileUpload: async ({ files }) => {
invariant(processFileUpload, "processFileUpload is required");

const response = await processFileUpload({ files });
return response;
},
responseTemplates: responseTemplates ?? [],
});

const threadManager = userThreadManager ?? defaultThreadManager;
const enableFileUpload = processFileUpload ? true : false;

useEffect(() => {
if (threadListManager.selectedThreadId) {
Expand All @@ -148,13 +159,15 @@ export const CrayonChat = ({
agentName={agentName}
messageLoadingComponent={messageLoadingComponent}
scrollVariant={scrollVariant}
enableFileUpload={enableFileUpload}
/>
) : (
<ComposedStandalone
logoUrl={logoUrl}
agentName={agentName}
messageLoadingComponent={messageLoadingComponent}
scrollVariant={scrollVariant}
enableFileUpload={enableFileUpload}
/>
)}
</ChatProvider>
Expand Down
Loading