Support HTML file uploads in Nova (#1076)

Adds .html/.htm extensions and text/html MIME to the upload accept list and validation. The API already ingests HTML files; the uploader was rejecting them client-side.
This commit is contained in:
MaheshtheDev 2026-06-10 06:09:20 +00:00
parent 8a9e3ff3b0
commit a068a794dd
3 changed files with 106 additions and 32 deletions

View file

@ -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"
@ -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
}
@ -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

View file

@ -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({
@ -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 &&
@ -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 && (

View file

@ -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 }) {
@ -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)
@ -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" ||