diff --git a/frontend/src/api/threads.ts b/frontend/src/api/threads.ts index 4e4e29c3..ffa1b879 100644 --- a/frontend/src/api/threads.ts +++ b/frontend/src/api/threads.ts @@ -1,4 +1,4 @@ -import { Chat } from "../hooks/useChatList.ts"; +import { Chat } from "../types"; export async function getThread(threadId: string): Promise { try { diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index b5a29ebf..796d99ed 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import { StreamStateProps } from "../hooks/useStreamState"; import { useChatMessages } from "../hooks/useChatMessages"; import TypingBox from "./TypingBox"; -import { Message } from "./Message"; +import { MessageViewer } from "./Message"; import { ArrowDownCircleIcon } from "@heroicons/react/24/outline"; import { MessageWithFiles } from "../utils/formTypes.ts"; import { useParams } from "react-router-dom"; @@ -51,7 +51,7 @@ export function Chat(props: ChatProps) { return (
{messages?.map((msg, i) => ( - ; + return url.protocol === "http:" || url.protocol === "https:"; } -function PageDocument(props: { document: PageDocument; className?: string }) { +function DocumentViewer(props: { + document: MessageDocument; + className?: string; +}) { const [open, setOpen] = useState(false); const metadata = useMemo(() => { @@ -46,28 +58,28 @@ function PageDocument(props: { document: PageDocument; className?: string }) { )} onClick={() => setOpen(true)} > - - - {props.document.page_content.trim().replace(/\n/g, " ")} - + + ); } return ( - - - {props.document.page_content} - + {metadata.map(({ key, value }, idx) => { @@ -77,22 +89,28 @@ function PageDocument(props: { document: PageDocument; className?: string }) { key={idx} > {key} - {value} + {isValidHttpUrl(value) ? ( + + {value} + + ) : ( + {value} + )} ); })} - +
); } -export function DocumentList(props: { documents: PageDocument[] }) { +export function DocumentList(props: { documents: MessageDocument[] }) { return (
{props.documents.map((document, idx) => ( - + ))}
diff --git a/frontend/src/components/Message.tsx b/frontend/src/components/Message.tsx index d691b449..2f1d1785 100644 --- a/frontend/src/components/Message.tsx +++ b/frontend/src/components/Message.tsx @@ -1,12 +1,12 @@ import { memo, useState } from "react"; -import { Message as MessageType } from "../hooks/useChatList"; +import { MessageDocument, Message as MessageType } from "../types"; import { str } from "../utils/str"; import { cn } from "../utils/cn"; -import { marked } from "marked"; -import DOMPurify from "dompurify"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { LangSmithActions } from "./LangSmithActions"; import { DocumentList } from "./Document"; +import { omit } from "lodash"; +import { StringViewer } from "./String"; function tryJsonParse(value: string) { try { @@ -88,14 +88,50 @@ function Function(props: { ); } -export const Message = memo(function Message( +function isDocumentContent( + content: MessageType["content"], +): content is MessageDocument[] { + return ( + Array.isArray(content) && + content.every((d) => typeof d === "object" && !!d && !!d.page_content) + ); +} + +export function MessageContent(props: { content: MessageType["content"] }) { + if (typeof props.content === "string") { + return ; + } else if (isDocumentContent(props.content)) { + return ; + } else if ( + Array.isArray(props.content) && + props.content.every( + (it) => typeof it === "object" && !!it && typeof it.content === "string", + ) + ) { + return ( + ({ + page_content: it.content, + metadata: omit(it, "content"), + }))} + /> + ); + } else { + return
{str(props.content)}
; + } +} + +export const MessageViewer = memo(function ( props: MessageType & { runId?: string }, ) { const [open, setOpen] = useState(false); const contentIsDocuments = ["function", "tool"].includes(props.type) && - Array.isArray(props.content) && - props.content.every((d) => !!d.page_content); + isDocumentContent(props.content); + const showContent = + ["function", "tool"].includes(props.type) && !contentIsDocuments + ? open + : true; return (
@@ -133,27 +169,7 @@ export const Message = memo(function Message( args={call.function?.arguments} /> ))} - {( - ["function", "tool"].includes(props.type) && !contentIsDocuments - ? open - : true - ) ? ( - typeof props.content === "string" ? ( -
- ) : contentIsDocuments ? ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - - ) : ( -
{str(props.content)}
- ) - ) : ( - false - )} + {showContent && }
{props.runId && ( diff --git a/frontend/src/components/String.tsx b/frontend/src/components/String.tsx new file mode 100644 index 00000000..b547e834 --- /dev/null +++ b/frontend/src/components/String.tsx @@ -0,0 +1,19 @@ +import { MarkedOptions, marked } from "marked"; +import DOMPurify from "dompurify"; +import { cn } from "../utils/cn"; + +const OPTIONS: MarkedOptions = { + gfm: true, + breaks: true, +}; + +export function StringViewer(props: { value: string; className?: string }) { + return ( +
+ ); +} diff --git a/frontend/src/components/TypingBox.tsx b/frontend/src/components/TypingBox.tsx index c369c9d6..df463e05 100644 --- a/frontend/src/components/TypingBox.tsx +++ b/frontend/src/components/TypingBox.tsx @@ -12,7 +12,7 @@ import { useDropzone } from "react-dropzone"; import { MessageWithFiles } from "../utils/formTypes.ts"; import { DROPZONE_CONFIG, TYPE_NAME } from "../constants.ts"; import { Config } from "../hooks/useConfigList.ts"; -import { Chat } from "../hooks/useChatList.ts"; +import { Chat } from "../types"; function getFileTypeIcon(fileType: string) { switch (fileType) { diff --git a/frontend/src/hooks/useChatList.ts b/frontend/src/hooks/useChatList.ts index 2e3d4123..b5a51bb6 100644 --- a/frontend/src/hooks/useChatList.ts +++ b/frontend/src/hooks/useChatList.ts @@ -1,37 +1,6 @@ import { useCallback, useEffect, useReducer } from "react"; import orderBy from "lodash/orderBy"; - -export interface Message { - id: string; - type: string; - content: - | string - | { page_content: string; metadata: Record }[] - | object; - name?: string; - additional_kwargs?: { - name?: string; - function_call?: { - name?: string; - arguments?: string; - }; - tool_calls?: { - id: string; - function?: { - name?: string; - arguments?: string; - }; - }[]; - }; - example: boolean; -} - -export interface Chat { - assistant_id: string; - thread_id: string; - name: string; - updated_at: string; -} +import { Chat } from "../types"; export interface ChatListProps { chats: Chat[] | null; diff --git a/frontend/src/hooks/useChatMessages.ts b/frontend/src/hooks/useChatMessages.ts index 28830af3..40db07ed 100644 --- a/frontend/src/hooks/useChatMessages.ts +++ b/frontend/src/hooks/useChatMessages.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { Message } from "./useChatList"; +import { Message } from "../types"; import { StreamState, mergeMessagesById } from "./useStreamState"; async function getState(threadId: string) { diff --git a/frontend/src/hooks/useStreamState.tsx b/frontend/src/hooks/useStreamState.tsx index 960f2c26..f0a4baee 100644 --- a/frontend/src/hooks/useStreamState.tsx +++ b/frontend/src/hooks/useStreamState.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { fetchEventSource } from "@microsoft/fetch-event-source"; -import { Message } from "./useChatList"; +import { Message } from "../types"; export interface StreamState { status: "inflight" | "error" | "done"; diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 00000000..f28259f5 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,32 @@ +export interface FunctionDefinition { + name?: string; + arguments?: string; +} + +export interface MessageDocument { + page_content: string; + metadata: Record; +} + +export interface Message { + id: string; + type: string; + content: string | MessageDocument[] | object; + name?: string; + additional_kwargs?: { + name?: string; + function_call?: FunctionDefinition; + tool_calls?: { + id: string; + function?: FunctionDefinition; + }[]; + }; + example: boolean; +} + +export interface Chat { + assistant_id: string; + thread_id: string; + name: string; + updated_at: string; +}