mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
fix: chat messages saving broken (#730)
1. New chat messages saving to old thread (race condition) 2. Feedback modal null handling issue (boolean | null not coerced to boolean) 3. Wrong icon on Integrations tab (was Cable, now Sun) 4. Wrong icon on Graph tab (was LayoutGridIcon, now GraphIcon) 5. Missing cursor pointer on header tabs 6. Default view was "graph" instead of "list"
This commit is contained in:
parent
727f177953
commit
b439e7ea01
7 changed files with 136 additions and 114 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
@ -174,10 +194,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`,
|
||||
credentials: "include",
|
||||
}),
|
||||
transport: chatTransport,
|
||||
onFinish: async (result) => {
|
||||
if (result.message.role !== "assistant") return
|
||||
|
||||
|
|
@ -376,9 +393,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)
|
||||
|
|
@ -492,14 +513,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]
|
||||
|
|
@ -867,12 +897,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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue