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/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ export default function NewPage() {
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
const isGraphMode = viewMode === "graph"
const showBottomNav = isMobile && !!session && !isChatView
const isPublicIntegrations = !session && viewMode === "integrations"

return (
<HotkeysProvider>
Expand All @@ -608,7 +609,9 @@ export default function NewPage() {
/>
</div>
)}
{!session && viewMode === "mcp" ? (
{isPublicIntegrations ? (
<PublicHeader variant="integrations" />
) : !session && viewMode === "mcp" ? (
<PublicHeader />
) : (
<Header
Expand Down Expand Up @@ -660,7 +663,7 @@ export default function NewPage() {
</div>
) : viewMode === "integrations" ? (
<div className="min-h-0 min-w-0 flex-1 p-4 pt-2! md:p-6 md:pr-0">
<IntegrationsView />
<IntegrationsView publicMode={isPublicIntegrations} />
</div>
) : viewMode === "mcp" ? (
<MCPDetailView
Expand Down
11 changes: 7 additions & 4 deletions apps/web/components/ensure-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams()
const { session, organizations, isRestoring } = useAuth()

const isMcpPublicPage = searchParams.get("view") === "mcp"
const isPublicAppPage =
pathname === "/" &&
["integrations", "mcp"].includes(searchParams.get("view") ?? "")
const isGuestPublicAppPage = isPublicAppPage && !session
const isOnboarding = pathname.startsWith("/onboarding")

useEffect(() => {
if (isMcpPublicPage) return
if (isGuestPublicAppPage) return
if (isRestoring) return
if (!session) {
router.replace(
Expand All @@ -44,11 +47,11 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
isRestoring,
isOnboarding,
router,
isMcpPublicPage,
isGuestPublicAppPage,
])

const showLoading =
!isMcpPublicPage &&
!isGuestPublicAppPage &&
(isRestoring ||
(!session && !isRestoring) ||
(session && organizations === null) ||
Expand Down
36 changes: 35 additions & 1 deletion apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,41 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
)
}

export function PublicHeader() {
export function PublicHeader({
variant = "default",
}: {
variant?: "default" | "integrations"
}) {
if (variant === "integrations") {
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-2.5 md:p-3">
<Link
href="/?view=integrations"
className="flex items-center gap-2 transition-opacity hover:opacity-90"
>
<Logo className="h-6 md:h-7" />
<p className="text-base leading-none font-medium text-white/90 sm:text-lg">
supermemory
</p>
</Link>

<Link href="/login?redirect=%2F%3Fview%3Dintegrations">
<button
type="button"
className={cn(
"flex h-10 cursor-pointer items-center gap-2 rounded-full px-4 text-[14px] font-medium text-white transition-opacity hover:opacity-95 sm:px-5 sm:text-[15px]",
"bg-[linear-gradient(100deg,#426BFF_0%,#2D1CFF_100%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.16)]",
dmSansClassName(),
)}
>
<Logo className="h-4 w-5 shrink-0" />
Log in with Supermemory
</button>
</Link>
</div>
)
}

return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-2.5 md:p-3">
<Link
Expand Down
74 changes: 61 additions & 13 deletions apps/web/components/integrations-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1460,13 +1460,18 @@ function SectionRail({
)
}

export function IntegrationsView() {
export function IntegrationsView({
publicMode = false,
}: {
publicMode?: boolean
}) {
const { setViewMode } = useViewMode()
const queryClient = useQueryClient()
const { org } = useAuth()
const autumn = useCustomer()
const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro")
const isAutumnLoading = autumn.isLoading
const autumn = useCustomer({ queryOptions: { enabled: !publicMode } })
const hasProProduct =
!publicMode && hasActivePlan(autumn.data?.subscriptions, "api_pro")
const isAutumnLoading = !publicMode && autumn.isLoading

const [connectingPlugin, setConnectingPlugin] = useState<string | null>(null)
const [connectingProvider, setConnectingProvider] =
Expand All @@ -1493,6 +1498,7 @@ export function IntegrationsView() {
if (!res.ok) throw new Error("Failed to fetch plugins")
return (await res.json()) as { plugins: string[] }
},
enabled: !publicMode,
queryKey: ["plugins"],
})

Expand All @@ -1507,7 +1513,7 @@ export function IntegrationsView() {
return response.data as Connection[]
},
staleTime: 30 * 1000,
enabled: hasProProduct,
enabled: !publicMode && hasProProduct,
})

const { data: apiKeys = [], refetch: refetchKeys } = useQuery<ListedApiKey[]>(
Expand All @@ -1524,7 +1530,7 @@ export function IntegrationsView() {
const data = (await res.json()) as { keys?: ListedApiKey[] }
return data.keys ?? []
},
enabled: !!org?.id,
enabled: !publicMode && !!org?.id,
staleTime: 30 * 1000,
},
)
Expand Down Expand Up @@ -1678,7 +1684,15 @@ export function IntegrationsView() {
}
}

const availablePluginIds = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)
const redirectToLogin = useCallback(() => {
const loginUrl = new URL("/login", window.location.origin)
loginUrl.searchParams.set("redirect", window.location.href)
window.location.assign(loginUrl.toString())
}, [])

const availablePluginIds = publicMode
? Object.keys(PLUGIN_CATALOG)
: (pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG))
const enabledPluginIds = new Set(
availablePluginIds.filter((id) => PLUGIN_CATALOG[id]),
)
Expand Down Expand Up @@ -1714,6 +1728,7 @@ export function IntegrationsView() {

const isItemConnected = useCallback(
(item: Item): boolean => {
if (publicMode) return false
if (item.kind === "plugin") {
return activePluginById.has(item.pluginId)
}
Expand All @@ -1722,7 +1737,7 @@ export function IntegrationsView() {
}
return false
},
[activePluginById, connectionsByProvider],
[activePluginById, connectionsByProvider, publicMode],
)

const counts = useMemo<Record<CategoryFilter, number>>(
Expand Down Expand Up @@ -1780,6 +1795,10 @@ export function IntegrationsView() {
),
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
window.open(POKE_RECIPE_URL, "_blank", "noopener,noreferrer")
},
},
Expand All @@ -1802,6 +1821,10 @@ export function IntegrationsView() {
docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction",
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
void setMcpClient(null)
setViewMode("mcp")
},
Expand Down Expand Up @@ -1831,12 +1854,18 @@ export function IntegrationsView() {
/>
),
docsUrl: "https://supermemory.ai/docs/integrations/claude-code",
ctaLabel: claudeCodeConnected
? "Active"
: claudeCodeNeedsPro
? "Upgrade"
: "Connect",
ctaLabel: publicMode
? "Connect"
: claudeCodeConnected
? "Active"
: claudeCodeNeedsPro
? "Upgrade"
: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
if (claudeCodeConnected) return
if (claudeCodeNeedsPro) {
handleUpgrade()
Expand All @@ -1855,6 +1884,10 @@ export function IntegrationsView() {
backdrop: <ChromeIcon className="size-96" />,
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer")
analytics.onboardingChromeExtensionClicked({ source: "integrations" })
},
Expand Down Expand Up @@ -1885,6 +1918,19 @@ export function IntegrationsView() {
})

const renderRight = (item: Item): ReactNode => {
if (publicMode) {
return (
<PillButton
onClick={() => {
trackCard(item)
redirectToLogin()
}}
>
Connect
</PillButton>
)
}

switch (item.kind) {
case "plugin": {
const activeKey = activePluginById.get(item.pluginId)
Expand Down Expand Up @@ -2065,6 +2111,8 @@ export function IntegrationsView() {
}

const renderStatus = (item: Item): ReactNode => {
if (publicMode) return null

switch (item.kind) {
case "plugin": {
const activeKey = activePluginById.get(item.pluginId)
Expand Down
8 changes: 7 additions & 1 deletion apps/web/components/memory-graph/graph-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { memo, useMemo } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Expand } from "lucide-react"
import { SuperLoader } from "@/components/superloader"
import { useGraphApi } from "./hooks/use-graph-api"
import { useViewMode } from "@/lib/view-mode-context"

Expand Down Expand Up @@ -157,7 +158,12 @@ export const GraphCard = memo<GraphCardProps>(
<div className="flex-1 w-full relative overflow-hidden rounded-lg">
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="size-6 border-2 border-blue-400/30 border-t-blue-400 rounded-full animate-spin" />
<SuperLoader
label="Loading graph..."
size={40}
colorClassName="text-[#4BA0FA]"
className="[&>span]:text-[#A8B0BD]"
/>
</div>
) : documentCount > 0 || memoryCount > 0 ? (
<StaticGraphPreview
Expand Down
18 changes: 15 additions & 3 deletions apps/web/components/memory-graph/memory-graph-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { MemoryGraph as MemoryGraphBase } from "@supermemory/memory-graph"
import type { GraphThemeColors } from "@supermemory/memory-graph"
import { SuperLoader } from "@/components/superloader"
import { useGraphApi } from "./hooks/use-graph-api"

export interface MemoryGraphWrapperProps {
Expand Down Expand Up @@ -46,14 +47,15 @@ export function MemoryGraph({
documentIds,
maxNodes,
})
const isInitialLoading = externalIsLoading || apiIsLoading

return (
<div className="absolute inset-0 [&>div]:!h-full [&>div]:!bg-none">
<MemoryGraphBase
documents={documents}
isLoading={externalIsLoading || apiIsLoading}
isLoadingMore={isLoadingMore}
onLoadMore={hasMore ? () => loadMore() : undefined}
isLoading={false}
isLoadingMore={false}
onLoadMore={hasMore && !isLoadingMore ? () => loadMore() : undefined}
hasMore={hasMore}
error={externalError || apiError}
variant={variant}
Expand All @@ -70,6 +72,16 @@ export function MemoryGraph({
>
{children}
</MemoryGraphBase>
{isInitialLoading && (
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center">
<SuperLoader
label="Loading memory graph..."
size={72}
colorClassName="text-[#4BA0FA]"
className="[&>span]:text-slate-100"
/>
</div>
)}
</div>
)
}
5 changes: 5 additions & 0 deletions apps/web/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export default async function proxy(request: Request) {
return NextResponse.next()
}

// Integrations index is public in guest mode; actions still require login.
if (url.pathname === "/" && url.searchParams.get("view") === "integrations") {
return NextResponse.next()
}

if (url.pathname.startsWith("/api/")) {
if (!sessionCookie) {
console.debug("[MIDDLEWARE] API route without session, returning 401")
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const ADD_MEMORY_SHORTCUT_URL =
const RAYCAST_EXTENSION_URL = "https://www.raycast.com/supermemory/supermemory"
const CHROME_EXTENSION_URL =
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc"
const POKE_RECIPE_URL = "https://poke.com/r/5tHPbS8gZvA"
const POKE_RECIPE_URL = "https://supermemory.link/poke"

export {
BIG_DIMENSIONS_NEW,
Expand Down
Loading