Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CSV display #3028

Merged
merged 6 commits into from
Nov 3, 2024
Merged
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 backend/danswer/file_store/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ChatFileType(str, Enum):
DOC = "document"
# Plain text only contain the text
PLAIN_TEXT = "plain_text"
CSV = "csv"


class FileDescriptor(TypedDict):
Expand Down
28 changes: 26 additions & 2 deletions backend/danswer/llm/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import json
from collections.abc import Callable
from collections.abc import Iterator
Expand All @@ -7,6 +8,7 @@
from typing import Union

import litellm # type: ignore
import pandas as pd
import tiktoken
from langchain.prompts.base import StringPromptValue
from langchain.prompts.chat import ChatPromptValue
Expand Down Expand Up @@ -135,6 +137,18 @@ def translate_history_to_basemessages(
return history_basemessages, history_token_counts


def _process_csv_file(file: InMemoryChatFile) -> str:
df = pd.read_csv(io.StringIO(file.content.decode("utf-8")))
csv_preview = df.head().to_string()

file_name_section = (
f"CSV FILE NAME: {file.filename}\n"
if file.filename
else "CSV FILE (NO NAME PROVIDED):\n"
)
return f"{file_name_section}{CODE_BLOCK_PAT.format(csv_preview)}\n\n\n"


def _build_content(
message: str,
files: list[InMemoryChatFile] | None = None,
Expand All @@ -145,16 +159,26 @@ def _build_content(
if files
else None
)
if not text_files:

csv_files = (
[file for file in files if file.file_type == ChatFileType.CSV]
if files
else None
)

if not text_files and not csv_files:
return message

final_message_with_files = "FILES:\n\n"
for file in text_files:
for file in text_files or []:
file_content = file.content.decode("utf-8")
file_name_section = f"DOCUMENT: {file.filename}\n" if file.filename else ""
final_message_with_files += (
f"{file_name_section}{CODE_BLOCK_PAT.format(file_content.strip())}\n\n\n"
)
for file in csv_files or []:
final_message_with_files += _process_csv_file(file)

final_message_with_files += message

return final_message_with_files
Expand Down
16 changes: 13 additions & 3 deletions backend/danswer/server/query_and_chat/chat_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,9 @@ def upload_files_for_chat(
_: User | None = Depends(current_user),
) -> dict[str, list[FileDescriptor]]:
image_content_types = {"image/jpeg", "image/png", "image/webp"}
csv_content_types = {"text/csv"}
text_content_types = {
"text/plain",
"text/csv",
"text/markdown",
"text/x-markdown",
"text/x-config",
Expand All @@ -578,8 +578,10 @@ def upload_files_for_chat(
"application/epub+zip",
}

allowed_content_types = image_content_types.union(text_content_types).union(
document_content_types
allowed_content_types = (
image_content_types.union(text_content_types)
.union(document_content_types)
.union(csv_content_types)
)

for file in files:
Expand All @@ -589,6 +591,10 @@ def upload_files_for_chat(
elif file.content_type in text_content_types:
error_detail = "Unsupported text file type. Supported text types include .txt, .csv, .md, .mdx, .conf, "
".log, .tsv."
elif file.content_type in csv_content_types:
error_detail = (
"Unsupported CSV file type. Supported CSV types include .csv."
)
else:
error_detail = (
"Unsupported document file type. Supported document types include .pdf, .docx, .pptx, .xlsx, "
Expand All @@ -614,6 +620,10 @@ def upload_files_for_chat(
file_type = ChatFileType.IMAGE
# Convert image to JPEG
file_content, new_content_type = convert_to_jpeg(file)
elif file.content_type in csv_content_types:
file_type = ChatFileType.CSV
file_content = io.BytesIO(file.file.read())
new_content_type = file.content_type or ""
elif file.content_type in document_content_types:
file_type = ChatFileType.DOC
file_content = io.BytesIO(file.file.read())
Expand Down
4 changes: 3 additions & 1 deletion backend/requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ types-urllib3==1.26.25.11
trafilatura==1.12.2
lxml==5.3.0
lxml_html_clean==0.2.2
boto3-stubs[s3]==1.34.133
boto3-stubs[s3]==1.34.133
pandas==2.2.3
pandas-stubs==2.2.3.241009
1 change: 1 addition & 0 deletions web/src/app/chat/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,7 @@ export function ChatPage({
const imageFiles = acceptedFiles.filter((file) =>
file.type.startsWith("image/")
);

if (imageFiles.length > 0 && !llmAcceptsImages) {
setPopup({
type: "error",
Expand Down
46 changes: 27 additions & 19 deletions web/src/app/chat/files/documents/DocumentPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,78 @@
import { FiFileText } from "react-icons/fi";
import { useState, useRef, useEffect } from "react";
import { Tooltip } from "@/components/tooltip/Tooltip";
import { ExpandTwoIcon } from "@/components/icons/icons";

export function DocumentPreview({
fileName,
maxWidth,
alignBubble,
open,
}: {
fileName: string;
open?: () => void;
maxWidth?: string;
alignBubble?: boolean;
}) {
const [isOverflowing, setIsOverflowing] = useState(false);
const fileNameRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (fileNameRef.current) {
setIsOverflowing(
fileNameRef.current.scrollWidth > fileNameRef.current.clientWidth
);
}
}, [fileName]);

return (
<div
className={`
${alignBubble && "w-64"}
flex
items-center
p-2
p-3
bg-hover
border
border-border
rounded-md
rounded-lg
box-border
h-16
h-20
hover:shadow-sm
transition-all
`}
>
<div className="flex-shrink-0">
<div
className="
w-12
h-12
w-14
h-14
bg-document
flex
items-center
justify-center
rounded-md
rounded-lg
transition-all
duration-200
hover:bg-document-dark
"
>
<FiFileText className="w-6 h-6 text-white" />
<FiFileText className="w-7 h-7 text-white" />
</div>
</div>
<div className="ml-4 relative">
<div className="ml-4 flex-grow">
<Tooltip content={fileName} side="top" align="start">
<div
ref={fileNameRef}
className={`font-medium text-sm line-clamp-1 break-all ellipses ${
className={`font-medium text-sm line-clamp-1 break-all ellipsis ${
maxWidth ? maxWidth : "max-w-48"
}`}
>
{fileName}
</div>
</Tooltip>
<div className="text-subtle text-sm">Document</div>
<div className="text-subtle text-xs mt-1">Document</div>
</div>
{open && (
<button
onClick={() => open()}
className="ml-2 p-2 rounded-full hover:bg-gray-200 transition-colors duration-200"
aria-label="Expand document"
>
<ExpandTwoIcon className="w-5 h-5 text-gray-600" />
</button>
)}
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions web/src/app/chat/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum ChatFileType {
IMAGE = "image",
DOCUMENT = "document",
PLAIN_TEXT = "plain_text",
CSV = "csv",
}

export interface FileDescriptor {
Expand Down
39 changes: 38 additions & 1 deletion web/src/app/chat/message/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import { LlmOverride } from "@/lib/hooks";
import { ContinueGenerating } from "./ContinueMessage";
import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents";
import { extractCodeText } from "./codeUtils";
import ToolResult from "../../../components/tools/ToolResult";
import CsvContent from "../../../components/tools/CSVContent";

const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME,
Expand All @@ -69,8 +71,13 @@ function FileDisplay({
files: FileDescriptor[];
alignBubble?: boolean;
}) {
const [close, setClose] = useState(true);
const imageFiles = files.filter((file) => file.type === ChatFileType.IMAGE);
const nonImgFiles = files.filter((file) => file.type !== ChatFileType.IMAGE);
const nonImgFiles = files.filter(
(file) => file.type !== ChatFileType.IMAGE && file.type !== ChatFileType.CSV
);

const csvImgFiles = files.filter((file) => file.type == ChatFileType.CSV);

return (
<>
Expand All @@ -94,6 +101,7 @@ function FileDisplay({
</div>
</div>
)}

{imageFiles && imageFiles.length > 0 && (
<div
id="danswer-image"
Expand All @@ -106,6 +114,35 @@ function FileDisplay({
</div>
</div>
)}

{csvImgFiles && csvImgFiles.length > 0 && (
<div className={` ${alignBubble && "ml-auto"} mt-2 auto mb-4`}>
<div className="flex flex-col gap-2">
{csvImgFiles.map((file) => {
return (
<div key={file.id} className="w-fit">
{close ? (
<>
<ToolResult
csvFileDescriptor={file}
close={() => setClose(false)}
contentComponent={CsvContent}
/>
</>
) : (
<DocumentPreview
open={() => setClose(true)}
fileName={file.name || file.id}
maxWidth="max-w-64"
alignBubble={alignBubble}
/>
)}
</div>
);
})}
</div>
</div>
)}
</>
);
}
Expand Down
22 changes: 18 additions & 4 deletions web/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { FiX } from "react-icons/fi";
import { IconProps, XIcon } from "./icons/icons";
import { useRef } from "react";
import { isEventWithinRef } from "@/lib/contains";
import ReactDOM from "react-dom";
import { useEffect, useState } from "react";

interface ModalProps {
icon?: ({ size, className }: IconProps) => JSX.Element;
Expand All @@ -14,6 +16,7 @@ interface ModalProps {
width?: string;
titleSize?: string;
hideDividerForTitle?: boolean;
hideCloseButton?: boolean;
noPadding?: boolean;
}

Expand All @@ -27,8 +30,17 @@ export function Modal({
hideDividerForTitle,
noPadding,
icon,
hideCloseButton,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
return () => {
setIsMounted(false);
};
}, []);

const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (
Expand All @@ -41,11 +53,11 @@ export function Modal({
}
};

return (
const modalContent = (
<div
onMouseDown={handleMouseDown}
className={`fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm h-full
flex items-center justify-center z-50 transition-opacity duration-300 ease-in-out`}
flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out`}
>
<div
ref={modalRef}
Expand All @@ -54,13 +66,13 @@ export function Modal({
e.stopPropagation();
}
}}
className={`bg-background text-emphasis rounded shadow-2xl
className={`bg-background text-emphasis rounded shadow-2xl
transform transition-all duration-300 ease-in-out
${width ?? "w-11/12 max-w-4xl"}
${noPadding ? "" : "p-10"}
${className || ""}`}
>
{onOutsideClick && (
{onOutsideClick && !hideCloseButton && (
<div className="absolute top-2 right-2">
<button
onClick={onOutsideClick}
Expand Down Expand Up @@ -93,4 +105,6 @@ export function Modal({
</div>
</div>
);

return isMounted ? ReactDOM.createPortal(modalContent, document.body) : null;
}
Loading
Loading