mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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:
parent
8a9e3ff3b0
commit
a068a794dd
3 changed files with 106 additions and 32 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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" ||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue