This commit is contained in:
Dhravya Shah 2026-02-12 16:20:03 -08:00
commit 405bff4d9f
7 changed files with 136 additions and 111 deletions

View file

@ -14,6 +14,7 @@ import Support from "@/components/new/settings/support"
import { useRouter } from "next/navigation"
import { useIsMobile } from "@hooks/use-mobile"
import { analytics } from "@/lib/analytics"
import { Sun } from "lucide-react"
const TABS = ["account", "integrations", "connections", "support"] as const
type SettingsTab = (typeof TABS)[number]
@ -52,23 +53,7 @@ const NAV_ITEMS: NavItem[] = [
id: "integrations",
label: "Integrations",
description: "Save, sync and search memories across tools",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="3" />
<path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" />
</svg>
),
icon: <Sun className="size-5" />,
},
{
id: "connections",

View file

@ -138,16 +138,36 @@ export function ChatSidebar({
)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const messagesContainerRef = useRef<HTMLDivElement>(null)
const sentQueuedMessageRef = useRef<string | null>(null)
const { selectedProject } = useProject()
const { viewMode } = useViewMode()
const { user } = useAuth()
const [threadId, setThreadId] = useQueryState("thread", threadParam)
const fallbackChatId = useMemo(() => generateId(), [])
const [fallbackChatId, setFallbackChatId] = useState(() => generateId())
const currentChatId = threadId ?? fallbackChatId
const chatIdRef = useRef(currentChatId)
chatIdRef.current = currentChatId
const setCurrentChatId = useCallback(
(id: string) => setThreadId(id),
[setThreadId],
)
const chatTransport = useMemo(
() =>
new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
prepareSendMessagesRequest: ({ messages }) => ({
body: {
messages,
metadata: {
chatId: chatIdRef.current,
projectId: selectedProject,
},
},
}),
}),
[currentChatId, selectedProject],
)
const [pendingThreadLoad, setPendingThreadLoad] = useState<{
id: string
messages: UIMessage[]
@ -175,7 +195,7 @@ export function ChatSidebar({
const { messages, sendMessage, status, setMessages, stop } = useChat({
id: currentChatId ?? undefined,
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
api: `${process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"}/chat/v2`,
credentials: "include",
body: {
metadata: {
@ -383,9 +403,13 @@ export function ChatSidebar({
const handleNewChat = useCallback(() => {
analytics.newChatCreated()
const newChatId = generateId()
chatIdRef.current = newChatId
setMessages([])
setThreadId(null)
setFallbackChatId(newChatId)
setInput("")
}, [setThreadId])
}, [setThreadId, setMessages])
const fetchThreads = useCallback(async () => {
setIsLoadingThreads(true)
@ -499,14 +523,23 @@ export function ChatSidebar({
isChatOpen &&
queuedMessage &&
status !== "submitted" &&
status !== "streaming"
status !== "streaming" &&
sentQueuedMessageRef.current !== queuedMessage
) {
sentQueuedMessageRef.current = queuedMessage
analytics.chatMessageSent({ source: "highlight" })
sendMessage({ text: queuedMessage })
onConsumeQueuedMessage?.()
}
}, [isChatOpen, queuedMessage, status, sendMessage, onConsumeQueuedMessage])
// Reset the sent message ref when queued message is consumed
useEffect(() => {
if (!queuedMessage) {
sentQueuedMessageRef.current = null
}
}, [queuedMessage])
// Scroll to bottom when a new user message is added
useEffect(() => {
const lastMessage = messages[messages.length - 1]
@ -874,12 +907,11 @@ export function ChatSidebar({
)}
</div>
))}
{(status === "submitted" || status === "streaming") &&
messages[messages.length - 1]?.role === "user" && (
<div className="flex gap-2">
<SuperLoader label="Thinking..." />
</div>
)}
{(status === "submitted" || status === "streaming") && (
<div className="flex gap-2">
<SuperLoader label="Thinking..." />
</div>
)}
</div>
</div>

View file

@ -11,7 +11,7 @@ import {
Settings,
Home,
Code2,
Cable,
Sun,
ExternalLink,
HelpCircle,
MenuIcon,
@ -22,6 +22,7 @@ import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { GraphIcon } from "@/components/new/integration-icons"
import {
DropdownMenu,
DropdownMenuContent,
@ -48,18 +49,18 @@ interface HeaderProps {
onOpenSearch?: () => void
}
export function Header({
onAddMemory,
onOpenChat,
onOpenSearch,
}: HeaderProps) {
export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
const { user } = useAuth()
const { selectedProject } = useProject()
const { switchProject } = useProjectMutations()
const router = useRouter()
const isMobile = useIsMobile()
const { resetOrgOnboarded } = useOrgOnboarding()
const [isFeedbackOpen, setIsFeedbackOpen] = useQueryState("feedback", feedbackParam)
const [feedbackOpen, setFeedbackOpen] = useQueryState(
"feedback",
feedbackParam,
)
const isFeedbackOpen = feedbackOpen ?? false
const { viewMode, setViewMode } = useViewMode()
const handleTryOnboarding = () => {
@ -67,9 +68,7 @@ export function Header({
router.push("/onboarding?step=input&flow=welcome")
}
const handleFeedback = () => {
setIsFeedbackOpen(true)
}
const handleFeedback = () => setFeedbackOpen(true)
const displayName =
user?.displayUsername ||
@ -155,13 +154,15 @@ export function Header({
{!isMobile && (
<Tabs
value={viewMode === "list" ? "grid" : viewMode}
onValueChange={(v) => setViewMode(v === "grid" ? "list" : v as "graph" | "integrations")}
onValueChange={(v) =>
setViewMode(v === "grid" ? "list" : (v as "graph" | "integrations"))
}
>
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
<TabsTrigger
value="grid"
className={cn(
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
@ -171,21 +172,21 @@ export function Header({
<TabsTrigger
value="graph"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<LayoutGridIcon className="size-4" />
<GraphIcon className="size-4" />
Graph
</TabsTrigger>
<TabsTrigger
value="integrations"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<Cable className="size-4" />
<Sun className="size-4" />
Integrations
</TabsTrigger>
</TabsList>
@ -232,7 +233,7 @@ export function Header({
onClick={() => setViewMode("integrations")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<Cable className="h-4 w-4 text-[#737373]" />
<Sun className="h-4 w-4 text-[#737373]" />
Integrations
</DropdownMenuItem>
<DropdownMenuItem
@ -410,7 +411,7 @@ export function Header({
</div>
<FeedbackModal
isOpen={isFeedbackOpen}
onClose={() => setIsFeedbackOpen(false)}
onClose={() => setFeedbackOpen(false)}
/>
</div>
)

View file

@ -1,4 +1,7 @@
import Image from "next/image"
import { Rotate3d } from "lucide-react"
export { Rotate3d as GraphIcon }
export function ChromeIcon({ className }: { className?: string }) {
return (

View file

@ -2,7 +2,7 @@
import { useState } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/new/onboarding/x-bookmarks-detail-view"
@ -17,7 +17,7 @@ import {
RaycastIcon,
} from "@/components/new/integration-icons"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { Cable, ArrowLeft } from "lucide-react"
import { ArrowLeft, Sun } from "lucide-react"
import Image from "next/image"
type CardId =
@ -45,9 +45,27 @@ const cards: IntegrationCardDef[] = [
pro: true,
icon: (
<div className="flex items-center -space-x-1.5">
<Image src="/images/plugins/claude-code.svg" alt="Claude Code" width={24} height={24} className="size-6 rounded" />
<Image src="/images/plugins/opencode.svg" alt="OpenCode" width={24} height={24} className="size-6 rounded" />
<Image src="/images/plugins/clawdbot.svg" alt="ClawdBot" width={24} height={24} className="size-6 rounded" />
<Image
src="/images/plugins/claude-code.svg"
alt="Claude Code"
width={24}
height={24}
className="size-6 rounded"
/>
<Image
src="/images/plugins/opencode.svg"
alt="OpenCode"
width={24}
height={24}
className="size-6 rounded"
/>
<Image
src="/images/plugins/clawdbot.svg"
alt="ClawdBot"
width={24}
height={24}
className="size-6 rounded"
/>
</div>
),
},
@ -69,11 +87,7 @@ const cards: IntegrationCardDef[] = [
title: "Connect to AI",
description: "Set up MCP to use your memory in Cursor, Claude, and more",
icon: (
<img
src="/onboarding/mcp.png"
alt="MCP"
className="size-20 h-auto"
/>
<img src="/onboarding/mcp.png" alt="MCP" className="size-20 h-auto" />
),
},
{
@ -98,9 +112,7 @@ const cards: IntegrationCardDef[] = [
id: "import",
title: "Import Bookmarks",
description: "Bring in X/Twitter bookmarks and turn them into memories",
icon: (
<img src="/onboarding/x.png" alt="X" className="size-10" />
),
icon: <img src="/onboarding/x.png" alt="X" className="size-10" />,
},
]
@ -131,46 +143,43 @@ function DetailWrapper({
export function IntegrationsView() {
const [selectedCard, setSelectedCard] = useState<CardId | null>(null)
if (selectedCard === "mcp") {
return <MCPDetailView onBack={() => setSelectedCard(null)} />
}
if (selectedCard === "import") {
return <XBookmarksDetailView onBack={() => setSelectedCard(null)} />
}
if (selectedCard === "chrome") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ChromeDetail />
</DetailWrapper>
)
}
if (selectedCard === "shortcuts") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ShortcutsDetail />
</DetailWrapper>
)
}
if (selectedCard === "raycast") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<RaycastDetail />
</DetailWrapper>
)
}
if (selectedCard === "connections") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ConnectionsDetail />
</DetailWrapper>
)
}
if (selectedCard === "plugins") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<PluginsDetail />
</DetailWrapper>
)
const handleBack = () => setSelectedCard(null)
switch (selectedCard) {
case "mcp":
return <MCPDetailView onBack={handleBack} />
case "import":
return <XBookmarksDetailView onBack={handleBack} />
case "chrome":
return (
<DetailWrapper onBack={handleBack}>
<ChromeDetail />
</DetailWrapper>
)
case "shortcuts":
return (
<DetailWrapper onBack={handleBack}>
<ShortcutsDetail />
</DetailWrapper>
)
case "raycast":
return (
<DetailWrapper onBack={handleBack}>
<RaycastDetail />
</DetailWrapper>
)
case "connections":
return (
<DetailWrapper onBack={handleBack}>
<ConnectionsDetail />
</DetailWrapper>
)
case "plugins":
return (
<DetailWrapper onBack={handleBack}>
<PluginsDetail />
</DetailWrapper>
)
}
return (
@ -178,15 +187,10 @@ export function IntegrationsView() {
<div className="max-w-3xl mx-auto">
<div className="mb-6 space-y-1">
<div className="flex items-center gap-2">
<Cable className="size-5 text-[#4BA0FA]" />
<Sun className="size-5 text-white" />
<h2 className="text-white text-xl font-medium">Integrations</h2>
</div>
<p
className={cn(
"text-[#8B8B8B] text-sm",
dmSansClassName(),
)}
>
<p className={cn("text-[#8B8B8B] text-sm", dmSansClassName())}>
Connect supermemory to your tools and workflows
</p>
</div>
@ -215,9 +219,7 @@ export function IntegrationsView() {
{card.icon}
</div>
<div className="flex-1">
<h3 className="text-white text-sm font-medium">
{card.title}
</h3>
<h3 className="text-white text-sm font-medium">{card.title}</h3>
<p
className={cn(
"text-[#8B8B8B] text-xs leading-relaxed mt-0.5",

View file

@ -25,6 +25,8 @@ export const feedbackParam = parseAsBoolean.withDefault(false)
// View & filter states
const viewLiterals = ["graph", "list", "integrations"] as const
export type ViewParamValue = (typeof viewLiterals)[number]
export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("graph")
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault([])
export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("list")
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
[],
)
export const projectParam = parseAsString.withDefault("sm_project_default")

View file

@ -58,7 +58,7 @@ function DialogContent({
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-100 sm:max-w-lg",
className,
)}
data-slot="dialog-content"