+
- {extension}
+ {fileName || extension}
{document.content && (
diff --git a/apps/web/components/document-icon.tsx b/apps/web/components/document-icon.tsx
index 202ead224..00e363414 100644
--- a/apps/web/components/document-icon.tsx
+++ b/apps/web/components/document-icon.tsx
@@ -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
+ }
+ switch (ext) {
+ case ".pdf":
+ return
+ case ".doc":
+ case ".docx":
+ return (
+
+
+
+ )
+ case ".xls":
+ case ".xlsx":
+ case ".csv":
+ return (
+
+
+
+ )
+ case ".ppt":
+ case ".pptx":
+ return (
+
+
+
+ )
+ case ".md":
+ case ".mdx":
+ case ".txt":
+ case ".json":
+ return
+ 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
}
+ // 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
+ }
+ if (!type || type === "unknown" || type === "text") {
+ return
+ }
+ }
+
if (
type === "webpage" ||
type === "url" ||
From 9ab2e7e64f7cf673e2528ca7c42b0031b96dbfdf Mon Sep 17 00:00:00 2001
From: Vedant Mahajan
Date: Wed, 10 Jun 2026 13:22:44 +0530
Subject: [PATCH 2/3] fix(web): load space profile from profile API (#1079)
---
apps/web/hooks/use-space-profile.ts | 42 +++++++++++++++++++++--------
1 file changed, 31 insertions(+), 11 deletions(-)
diff --git a/apps/web/hooks/use-space-profile.ts b/apps/web/hooks/use-space-profile.ts
index 9ee2114d9..8883625fb 100644
--- a/apps/web/hooks/use-space-profile.ts
+++ b/apps/web/hooks/use-space-profile.ts
@@ -1,5 +1,4 @@
import { useQuery } from "@tanstack/react-query"
-import { $fetch } from "@lib/api"
import { useAuth } from "@lib/auth-context"
export type SpaceProfile = {
@@ -7,6 +6,16 @@ export type SpaceProfile = {
dynamic: string[]
}
+const API_BASE =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+
+type SpaceProfileResponse = {
+ profile?: {
+ static?: string[] | null
+ dynamic?: string[] | null
+ } | null
+}
+
export function useSpaceProfile(containerTag: string) {
const { org } = useAuth()
const orgId = org?.id ?? ""
@@ -14,21 +23,32 @@ export function useSpaceProfile(containerTag: string) {
return useQuery({
queryKey: ["space-profile", orgId, containerTag],
queryFn: async (): Promise => {
- const response = await $fetch(
- "@get/container-tags/:containerTag/profile",
- {
- params: { containerTag },
+ const response = await fetch(`${API_BASE}/v4/profile`, {
+ method: "POST",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ "X-App-Source": "nova",
},
- )
- if (response.error) {
+ body: JSON.stringify({ containerTag }),
+ })
+
+ if (!response.ok) {
+ const body = (await response.json().catch(() => ({}))) as {
+ error?: string
+ message?: string
+ }
throw new Error(
- response.error.message || "Failed to load space profile",
+ body.message ?? body.error ?? "Failed to load space profile",
)
}
- const profile = response.data.profile
+
+ const data = (await response.json()) as SpaceProfileResponse
+ const profile = data.profile
+
return {
- static: profile.static ?? [],
- dynamic: profile.dynamic ?? [],
+ static: profile?.static ?? [],
+ dynamic: profile?.dynamic ?? [],
}
},
enabled: !!orgId && !!containerTag,
From 9a1bab718292310bca69e5694fc1ef2096377c82 Mon Sep 17 00:00:00 2001
From: Mahesh Sanikommu
Date: Wed, 10 Jun 2026 01:04:33 -0700
Subject: [PATCH 3/3] feat(billing): cancel flow with reason capture +
book-a-call (#1077)
Co-authored-by: ved015
---
apps/web/components/settings/billing.tsx | 315 +++++++++++++-----
.../web/components/settings/cancel-reasons.ts | 19 ++
2 files changed, 254 insertions(+), 80 deletions(-)
create mode 100644 apps/web/components/settings/cancel-reasons.ts
diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx
index 2021f83b2..3627b2204 100644
--- a/apps/web/components/settings/billing.tsx
+++ b/apps/web/components/settings/billing.tsx
@@ -11,8 +11,15 @@ import {
DialogContent,
DialogTrigger,
} from "@ui/components/dialog"
+import { Logo } from "@ui/assets/Logo"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { useCustomer } from "autumn-js/react"
+import { usePostHog } from "@lib/posthog"
+import {
+ CANCEL_REASONS,
+ cancelReasonNeedsDetail,
+ type CancelReasonValue,
+} from "./cancel-reasons"
import {
Check,
ChevronLeft,
@@ -31,6 +38,38 @@ import { toast } from "sonner"
const API_BASE =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+const BOOK_CALL_HREF = "https://cal.com/maheshthedev/15min"
+
+function GoogleMeetIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
const CREDIT_FEATURE_ID = "usd_credits"
const TOP_UP_PLAN_ID = "credits_topup"
const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const
@@ -437,11 +476,15 @@ export default function Billing() {
const queryClient = useQueryClient()
const { user, org } = useAuth()
const autumn = useCustomer()
+ const posthog = usePostHog()
const [isUpgrading, setIsUpgrading] = useState(false)
const [isCancelling, setIsCancelling] = useState(false)
const [isResuming, setIsResuming] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
- const [cancelConfirmText, setCancelConfirmText] = useState("")
+ const [cancelReason, setCancelReason] = useState(
+ null,
+ )
+ const [cancelDetail, setCancelDetail] = useState("")
const [isCreditsDialogOpen, setIsCreditsDialogOpen] = useState(false)
const [isPlanCarouselActive, setIsPlanCarouselActive] = useState(false)
const [planPage, setPlanPage] = useState<0 | 1>(0)
@@ -578,16 +621,11 @@ export default function Billing() {
? (`api_${currentPlan}` as const)
: null
- const currentPlanCard = [...PLAN_CARDS, ...ADVANCED_PLAN_CARDS].find(
- (p) => p.id === currentPlan,
- )
- const cancelLossItems = currentPlanCard
- ? [
- `${currentPlanCard.credits}/mo included credits`,
- ...currentPlanCard.features.filter((f) => !/credit/i.test(f)),
- ]
- : []
- const canConfirmCancel = cancelConfirmText.trim().toUpperCase() === "CANCEL"
+ const cancelNeedsDetail =
+ cancelReason != null && cancelReasonNeedsDetail(cancelReason)
+ const canConfirmCancel =
+ cancelReason != null &&
+ (!cancelNeedsDetail || cancelDetail.trim().length > 0)
const canceledSub = getCanceledSubscription(autumn.data?.subscriptions)
const isPlanCanceling = canceledSub != null
@@ -627,6 +665,11 @@ export default function Billing() {
}
}
+ const resetCancelForm = () => {
+ setCancelReason(null)
+ setCancelDetail("")
+ }
+
const handleCancelSubscription = async () => {
if (!cancellablePlanId) return
setIsCancelling(true)
@@ -635,9 +678,18 @@ export default function Billing() {
planId: cancellablePlanId,
cancelAction: "cancel_end_of_cycle",
})
+ if (posthog?.__loaded) {
+ posthog.capture("subscription_cancelled", {
+ reason: cancelReason,
+ reason_detail: cancelDetail.trim() || null,
+ plan: currentPlan,
+ plan_id: cancellablePlanId,
+ surface: "nova",
+ })
+ }
autumn.refetch?.()
setIsCancelDialogOpen(false)
- setCancelConfirmText("")
+ resetCancelForm()
toast.success(
`Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`,
)
@@ -934,7 +986,7 @@ export default function Billing() {
open={isCancelDialogOpen}
onOpenChange={(open) => {
setIsCancelDialogOpen(open)
- if (!open) setCancelConfirmText("")
+ if (!open) resetCancelForm()
}}
>
@@ -950,7 +1002,7 @@ export default function Billing() {
@@ -985,74 +1037,177 @@ export default function Billing() {
- {cancelLossItems.length > 0 ? (
-
-
- You'll lose
-
-
- {cancelLossItems.map((item) => (
- -
+
+
+
+
+
+ or
+
+
+
+
+
+ Why are you leaving?
+
+
+ {CANCEL_REASONS.map((option) => {
+ const selected = cancelReason === option.value
+ return (
+
+ )
+ })}
+
+ {cancelReason !== null ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
- ) : null}
-
-
- setCancelConfirmText(e.target.value)}
- placeholder="CANCEL"
- type="text"
- value={cancelConfirmText}
- />
-
-
-
-
-
-
diff --git a/apps/web/components/settings/cancel-reasons.ts b/apps/web/components/settings/cancel-reasons.ts
new file mode 100644
index 000000000..9378193be
--- /dev/null
+++ b/apps/web/components/settings/cancel-reasons.ts
@@ -0,0 +1,19 @@
+export const CANCEL_REASONS = [
+ { value: "too_expensive", label: "Too expensive" },
+ { value: "missing_features", label: "Missing features I need" },
+ { value: "switching", label: "Found a better alternative" },
+ { value: "not_using", label: "Not using it enough" },
+ { value: "other", label: "Other" },
+] as const
+
+export type CancelReasonValue = (typeof CANCEL_REASONS)[number]["value"]
+
+const NEEDS_DETAIL: CancelReasonValue[] = [
+ "missing_features",
+ "switching",
+ "other",
+]
+
+export function cancelReasonNeedsDetail(value: CancelReasonValue): boolean {
+ return NEEDS_DETAIL.includes(value)
+}