Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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(false);
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
14 changes: 7 additions & 7 deletions web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ToggleSwitch = () => {
return (
<div className="bg-background-toggle mobile:mt-8 flex rounded-full p-1">
<div
className={`absolute mobile:mt-8 top-1 bottom-1 ${
className={` mobile:mt-8 top-1 bottom-1 ${
activeTab === "chat" ? "w-[45%]" : "w-[50%]"
} bg-white rounded-full shadow ${
isInitialLoad ? "" : "transition-transform duration-300 ease-in-out"
Expand All @@ -53,7 +53,7 @@ const ToggleSwitch = () => {
onClick={() => handleTabChange("search")}
>
<SearchIcon size={16} className="mr-2" />
<div className="flex items-center">
<div className="flex items-center">
Search
<div className="ml-2 flex content-center">
<span className="leading-none pb-[1px] my-auto">
Expand Down Expand Up @@ -145,10 +145,14 @@ export default function FunctionalWrapper({

return (
<>
<div className="overscroll-y-contain overflow-y-scroll z-50 overscroll-contain left-0 top-0 w-full h-svh">
{content(toggledSidebar, toggle)}
</div>

{(!settings ||
(settings.search_page_enabled && settings.chat_page_enabled)) && (
<div
className={`mobile:hidden z-30 flex fixed ${
className={`mobile:hidden flex absolute ${
chatBannerPresent ? (twoLines ? "top-20" : "top-14") : "top-4"
} left-1/2 transform -translate-x-1/2`}
>
Expand All @@ -162,10 +166,6 @@ export default function FunctionalWrapper({
</div>
</div>
)}

<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
{content(toggledSidebar, toggle)}
</div>
</>
);
}
Loading
Loading