Skip to content

Commit

Permalink
Merge pull request #303 from langchain-ai/nc/15apr/render-search-tool…
Browse files Browse the repository at this point in the history
…-output

Render output of search tool using DocumentList and markdown parser
  • Loading branch information
nfcampos authored Apr 15, 2024
2 parents c16e82d + e9a4b56 commit 950719c
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 84 deletions.
2 changes: 1 addition & 1 deletion frontend/src/api/threads.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chat } from "../hooks/useChatList.ts";
import { Chat } from "../types";

export async function getThread(threadId: string): Promise<Chat | null> {
try {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,7 +51,7 @@ export function Chat(props: ChatProps) {
return (
<div className="flex-1 flex flex-col items-stretch pb-[76px] pt-2">
{messages?.map((msg, i) => (
<Message
<MessageViewer
{...msg}
key={msg.id}
runId={
Expand Down
56 changes: 37 additions & 19 deletions frontend/src/components/Document.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { useMemo, useState } from "react";
import { cn } from "../utils/cn";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { cn } from "../utils/cn";
import { MessageDocument } from "../types";
import { StringViewer } from "./String";

function isValidHttpUrl(str: string) {
let url;

try {
url = new URL(str);
} catch (_) {
return false;
}

export interface PageDocument {
page_content: string;
metadata: Record<string, unknown>;
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(() => {
Expand Down Expand Up @@ -46,28 +58,28 @@ function PageDocument(props: { document: PageDocument; className?: string }) {
)}
onClick={() => setOpen(true)}
>
<ChevronRightIcon className="mt-1 h-4 w-4 text-gray-500" />
<span className="min-w-0 flex-grow basis-0 overflow-hidden text-ellipsis whitespace-nowrap text-left">
{props.document.page_content.trim().replace(/\n/g, " ")}
</span>
<ChevronRightIcon className="mt-[6px] h-4 w-4 text-gray-500" />
<StringViewer
className="min-w-0 flex-grow basis-0 overflow-hidden text-ellipsis whitespace-nowrap text-left max-w-none"
value={props.document.page_content.trim().replace(/\n/g, " ")}
/>
</button>
);
}

return (
<button
<div
className={cn(
"flex items-start gap-4 px-4 text-left transition-colors hover:bg-gray-50/50 active:bg-gray-50",
props.className,
)}
onClick={() => setOpen(false)}
>
<ChevronDownIcon className="mt-1 h-4 w-4 text-gray-500" />
<button onClick={() => setOpen(false)}>
<ChevronDownIcon className="mt-[6px] h-4 w-4 text-gray-500" />
</button>

<span className="flex flex-grow basis-0 flex-col gap-4">
<span className="whitespace-pre-line">
{props.document.page_content}
</span>
<StringViewer value={props.document.page_content} />

<span className="flex flex-col flex-wrap items-start gap-2">
{metadata.map(({ key, value }, idx) => {
Expand All @@ -77,22 +89,28 @@ function PageDocument(props: { document: PageDocument; className?: string }) {
key={idx}
>
<span className="mr-1.5 font-mono font-bold">{key}</span>
<span className="whitespace-pre-wrap">{value}</span>
{isValidHttpUrl(value) ? (
<a href={value} target="_blank" rel="noreferrer">
{value}
</a>
) : (
<span className="whitespace-pre-wrap">{value}</span>
)}
</span>
);
})}
</span>
</span>
</button>
</div>
);
}

export function DocumentList(props: { documents: PageDocument[] }) {
export function DocumentList(props: { documents: MessageDocument[] }) {
return (
<div className="flex flex-col items-stretch gap-4 rounded-lg ring-1 ring-gray-300 overflow-hidden my-2">
<div className="grid divide-y empty:hidden">
{props.documents.map((document, idx) => (
<PageDocument document={document} key={idx} className="py-3" />
<DocumentViewer document={document} key={idx} className="py-3" />
))}
</div>
</div>
Expand Down
70 changes: 43 additions & 27 deletions frontend/src/components/Message.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 <StringViewer value={props.content} />;
} else if (isDocumentContent(props.content)) {
return <DocumentList documents={props.content} />;
} else if (
Array.isArray(props.content) &&
props.content.every(
(it) => typeof it === "object" && !!it && typeof it.content === "string",
)
) {
return (
<DocumentList
documents={props.content.map((it) => ({
page_content: it.content,
metadata: omit(it, "content"),
}))}
/>
);
} else {
return <div className="text-gray-900 prose">{str(props.content)}</div>;
}
}

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 (
<div className="flex flex-col mb-8">
<div className="leading-6 flex flex-row">
Expand Down Expand Up @@ -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" ? (
<div
className="text-gray-900 prose"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(marked(props.content)).trim(),
}}
/>
) : contentIsDocuments ? (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<DocumentList documents={props.content as any} />
) : (
<div className="text-gray-900 prose">{str(props.content)}</div>
)
) : (
false
)}
{showContent && <MessageContent content={props.content} />}
</div>
</div>
{props.runId && (
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/String.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn("text-gray-900 prose", props.className)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(marked(props.value, OPTIONS)).trim(),
}}
/>
);
}
2 changes: 1 addition & 1 deletion frontend/src/components/TypingBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 1 addition & 32 deletions frontend/src/hooks/useChatList.ts
Original file line number Diff line number Diff line change
@@ -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<string, object> }[]
| 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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useChatMessages.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useStreamState.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface FunctionDefinition {
name?: string;
arguments?: string;
}

export interface MessageDocument {
page_content: string;
metadata: Record<string, unknown>;
}

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;
}

0 comments on commit 950719c

Please sign in to comment.