Skip to content
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
7 changes: 5 additions & 2 deletions apps/web/components/add-document/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useHotkeys } from "react-hotkeys-hook"
import { toast } from "sonner"

export const FILE_ACCEPT =
"image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,text/markdown,application/json"
"image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,.html,.htm,text/markdown,application/json,text/html"

export type FileQueueItemStatus = "pending" | "uploading" | "success" | "error"

Expand Down Expand Up @@ -47,11 +47,14 @@ function isAcceptedFile(file: File): boolean {
".md",
".mdx",
".json",
".html",
".htm",
])
if (allowedExt.has(ext)) return true
if (file.type.startsWith("image/")) return true
if (file.type === "text/markdown") return true
if (file.type === "application/json") return true
if (file.type === "text/html") return true
return false
}

Expand Down Expand Up @@ -211,7 +214,7 @@ export function FileContent({
<div className="flex flex-col gap-0.5 pl-2">
<p className="text-[16px] font-medium">Upload files</p>
<p className="text-[#737373] text-xs">
Images, PDF, documents, sheets, markdown
Images, PDF, documents, sheets, markdown, HTML
</p>
</div>
<label
Expand Down
66 changes: 37 additions & 29 deletions apps/web/components/document-cards/file-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,37 @@ type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

function getFileTypeInfo(document: DocumentWithMemories): {
fileName?: string
extension: string
color?: string
} {
const type = document.type?.toLowerCase()
const mimeType = document.metadata?.mimeType as string | undefined
const mimeType = (document.metadata?.mimeType ??
document.metadata?.sm_internal_fileType) as string | undefined
const fileName = (document.metadata?.sm_internal_fileName ??
document.metadata?.fileName) as string | undefined
const nameExt = fileName?.includes(".")
? fileName.slice(fileName.lastIndexOf(".")).toLowerCase()
: undefined

if (mimeType) {
if (mimeType === "application/pdf") {
return { extension: ".pdf", color: "#FF7673" }
}
if (mimeType.startsWith("image/")) {
const ext = mimeType.split("/")[1] || "jpg"
return { extension: `.${ext}` }
}
if (mimeType.startsWith("video/")) {
const ext = mimeType.split("/")[1] || "mp4"
return { extension: `.${ext}` }
}
if (nameExt === ".pdf" || mimeType === "application/pdf" || type === "pdf") {
return { fileName, extension: ".pdf", color: "#FF7673" }
}

switch (type) {
case "pdf":
return { extension: ".pdf", color: "#FF7673" }
case "image":
return { extension: ".jpg" }
case "video":
return { extension: ".mp4" }
default:
return { extension: ".file" }
if (mimeType?.startsWith("image/") || type === "image") {
const ext = nameExt || `.${mimeType?.split("/")[1] || "jpg"}`
return { fileName, extension: ext }
}
if (mimeType?.startsWith("video/") || type === "video") {
const ext = nameExt || `.${mimeType?.split("/")[1] || "mp4"}`
return { fileName, extension: ext }
}
if (nameExt === ".html" || nameExt === ".htm" || mimeType === "text/html") {
return { fileName, extension: nameExt || ".html", color: "#FF8A4C" }
}
if (nameExt) {
return { fileName, extension: nameExt }
}
return { fileName, extension: ".file" }
}

export const FilePreview = memo(function FilePreview({
Expand All @@ -50,10 +51,11 @@ export const FilePreview = memo(function FilePreview({
}) {
const [imageError, setImageError] = useState(false)
const [retryKey, setRetryKey] = useState(0)
const { extension, color } = getFileTypeInfo(document)
const { fileName, extension, color } = getFileTypeInfo(document)

const type = document.type?.toLowerCase()
const mimeType = document.metadata?.mimeType as string | undefined
const mimeType = (document.metadata?.mimeType ??
document.metadata?.sm_internal_fileType) as string | undefined
const isImage =
(mimeType?.startsWith("image/") || type === "image") &&
document.url &&
Expand Down Expand Up @@ -102,17 +104,23 @@ export const FilePreview = memo(function FilePreview({
</div>
) : (
<div className="p-3">
<div className="flex items-center gap-1 mb-2">
<div className="flex items-center gap-1 mb-2 min-w-0">
<DocumentIcon
type={document.type}
url={document.url}
className="size-4"
fileName={fileName}
mimeType={mimeType}
className="size-4 shrink-0"
/>
<p
className={cn(dmSansClassName(), "text-[11px] font-semibold")}
className={cn(
dmSansClassName(),
"text-[11px] font-semibold truncate",
)}
style={{ color: color }}
title={fileName}
>
{extension}
{fileName || extension}
</p>
</div>
{document.content && (
Expand Down
65 changes: 64 additions & 1 deletion apps/web/components/document-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
NotionDoc,
PDF,
} from "@ui/assets/icons"
import { Globe, FileText, Image } from "lucide-react"
import { Globe, FileText, FileCode, Image } from "lucide-react"
import { cn } from "@lib/utils"

function MCPIcon({ className }: { className?: string }) {
Expand Down Expand Up @@ -144,13 +144,60 @@ export interface DocumentIconProps {
type: string | null | undefined
source?: string | null
url?: string | null
fileName?: string | null
mimeType?: string | null
className?: string
}

function fileExtensionIcon(
ext: string | undefined,
mimeType: string | null | undefined,
iconClassName: string,
): React.ReactNode | null {
if (ext === ".html" || ext === ".htm" || mimeType === "text/html") {
return <FileCode className={iconClassName} style={{ color: "#FF8A4C" }} />
}
switch (ext) {
case ".pdf":
return <PDF className={iconClassName} />
case ".doc":
case ".docx":
return (
<span style={{ color: BRAND_COLORS.word }}>
<MicrosoftWord className={iconClassName} />
</span>
)
case ".xls":
case ".xlsx":
case ".csv":
return (
<span style={{ color: BRAND_COLORS.excel }}>
<MicrosoftExcel className={iconClassName} />
</span>
)
case ".ppt":
case ".pptx":
return (
<span style={{ color: BRAND_COLORS.powerpoint }}>
<MicrosoftPowerpoint className={iconClassName} />
</span>
)
case ".md":
case ".mdx":
case ".txt":
case ".json":
return <TextDocumentIcon className={iconClassName} />
default:
return null
}
}

export function DocumentIcon({
type,
source,
url,
fileName,
mimeType,
className,
}: DocumentIconProps) {
const iconClassName = cn("size-4", className)
Expand All @@ -163,6 +210,22 @@ export function DocumentIcon({
return <YouTubeIcon className={iconClassName} />
}

// Uploaded files get a type icon, never the URL favicon of their storage host
if (fileName || mimeType) {
const lower = fileName?.toLowerCase()
const ext = lower?.includes(".")
? lower.slice(lower.lastIndexOf("."))
: undefined
const fileIcon = fileExtensionIcon(ext, mimeType, iconClassName)
if (fileIcon) return fileIcon
if (mimeType?.startsWith("image/")) {
return <Image className={iconClassName} style={{ color: "#FAFAFA" }} />
}
if (!type || type === "unknown" || type === "text") {
return <FileText className={iconClassName} style={{ color: "#FAFAFA" }} />
}
}

if (
type === "webpage" ||
type === "url" ||
Expand Down
Loading
Loading