This commit is contained in:
Dhravya Shah 2025-10-07 17:25:59 -07:00
commit 290f731b51
27 changed files with 1515 additions and 1044 deletions

View file

@ -7,7 +7,7 @@ import { ChatMessages } from "@/components/views/chat/chat-messages"
export default function ChatPage() {
const params = useParams()
const { setCurrentChatId, getCurrentChat } = usePersistentChat()
const { setCurrentChatId } = usePersistentChat()
const chatId = params.id as string
@ -17,17 +17,9 @@ export default function ChatPage() {
}
}, [chatId, setCurrentChatId])
const currentChat = getCurrentChat()
return (
<div className="flex flex-col w-full">
<div className="flex flex-col h-[93vh]">
<div className="flex justify-center w-full">
<h1 className="text-lg font-semibold flex-1 truncate text-center">
{currentChat?.title || "New Chat"}
</h1>
</div>
<div className="flex-1 flex justify-center min-h-0 w-full px-4">
<div className="flex flex-col min-h-0 w-full max-w-4xl">
<ChatMessages />

View file

@ -69,7 +69,7 @@ export default function SupportPage() {
What's included in the Pro plan?
</h4>
<p className="text-muted-foreground text-sm">
Pro includes 5,000 memories (vs 200 in free), 10 connections to
Pro includes unlimited memories (vs 200 in free), 10 connections to
external services like Google Drive and Notion, advanced search
features, and priority support.
</p>

View file

@ -31,23 +31,23 @@ export function BioForm() {
})
}
return (
<div className="relative">
<div className="relative w-full">
<div className="space-y-4">
<NavMenu>
<p className="text-base text-white/60">
Step {getStepNumberFor("bio")} of {totalSteps}
</p>
</NavMenu>
<h1 className="max-sm:text-4xl text-white font-medium">
Tell us about yourself
<h1 className="text-2xl md:text-4xl text-white font-medium">
Tell Supermemory about yourself
</h1>
<p className="text-2xl max-sm:text-lg text-white/80">
What should Supermemory know about you?
<p className="text-lg md:text-xl text-white/80">
share with Supermemory what you do, who you are, and what you're interested in
</p>
</div>
<Textarea
autoFocus
className="font-sans mt-6 text-base! text-white tracking-normal font-medium border bg-white/30 border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)]"
className="font-sans mt-6 text-base! placeholder:text-white/80 text-white tracking-normal font-medium border bg-white/30 border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)] w-full"
placeholder="I'm a software engineer from San Francisco..."
rows={3}
value={bio}
@ -61,12 +61,12 @@ export function BioForm() {
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="flex justify-end mt-2 absolute -bottom-12 right-0"
className="flex justify-end mt-4 md:mt-2 md:absolute md:-bottom-12 md:right-0"
>
<Button
variant="link"
size="lg"
className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
className="text-white/80 font-medium! text-base md:text-lg underline w-fit px-0! cursor-pointer"
onClick={handleNext}
>
Save & Continue
@ -79,12 +79,12 @@ export function BioForm() {
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(5px)" }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="flex justify-end mt-2 absolute -bottom-12 right-0"
className="flex justify-end mt-4 md:mt-2 md:absolute md:-bottom-12 md:right-0"
>
<Button
variant="link"
size="lg"
className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
className="text-white/80 font-medium! text-base md:text-lg underline w-fit px-0! cursor-pointer"
onClick={handleNext}
>
Skip For Now

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ export function Intro() {
return (
<motion.div
className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full text-white text-center"
className="flex flex-col gap-4 relative text-2xl md:text-4xl w-full text-white text-center"
layout
transition={{
layout: { duration: 0.8, ease: "anticipate" },

View file

@ -220,7 +220,7 @@ export function MCPForm() {
<Button
variant="link"
size="lg"
className="text-black/40 font-medium! text-lg underline w-fit px-0! cursor-pointer"
className="text-white not-odd:font-medium! text-lg underline w-fit px-0! cursor-pointer"
onClick={nextStep}
>
Continue
@ -238,7 +238,7 @@ export function MCPForm() {
<Button
variant="link"
size="lg"
className="text-black/40 font-medium! text-lg underline w-fit px-0! cursor-pointer"
className="text-white font-medium! text-lg underline w-fit px-0! cursor-pointer"
onClick={nextStep}
>
Skip For Now

View file

@ -1,11 +1,9 @@
"use client"
import { Button } from "@repo/ui/components/button"
import { useOnboarding } from "./onboarding-context"
import { useAuth } from "@lib/auth-context"
import Link from "next/link"
import { useEffect, useState } from "react"
import { Input } from "@ui/components/input"
import { CheckIcon } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { NavMenu } from "./nav-menu"
@ -20,8 +18,7 @@ export function NameForm() {
if (!name && user?.name) {
setName(user.name)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.name])
}, [name, user?.name])
function handleNext(): void {
const trimmed = name.trim()
@ -51,13 +48,15 @@ export function NameForm() {
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 w-full">
<NavMenu>
<p className="text-base text-white/60">
Step {getStepNumberFor("name")} of {totalSteps}
</p>
</NavMenu>
<p className="text-4xl text-white font-medium">What should we call you?</p>
<p className="text-2xl md:text-4xl text-white font-medium">
What should we call you?
</p>
<form onSubmit={handleSubmit} className="flex flex-col group text-white">
<div className="relative flex flex-col">

View file

@ -17,7 +17,7 @@ export function OnboardingBackground({ children }: OnboardingBackgroundProps) {
currentZoomStepIndex >= 0 ? 1.0 + currentZoomStepIndex * 0.1 : 1.0
return (
<div className="min-h-screen w-full overflow-x-hidden text-zinc-900 flex items-center justify-center relative">
<div className="min-h-screen w-full overflow-x-hidden text-zinc-900 flex items-center justify-center relative px-4 md:px-0">
<div
className="absolute inset-0 transition-transform duration-700 ease-in-out"
style={{
@ -26,10 +26,9 @@ export function OnboardingBackground({ children }: OnboardingBackgroundProps) {
backgroundPosition: "bottom",
backgroundRepeat: "no-repeat",
transform: `scale(${zoomScale})`,
transformOrigin: "center bottom",
}}
/>
<div className="relative z-10">{children}</div>
<div className="relative z-10 w-full max-w-4xl mx-auto">{children}</div>
</div>
)
}

View file

@ -22,7 +22,7 @@ export function OnboardingForm() {
return (
<div
className={cn(
"text-4xl px-6 py-8 flex flex-col justify-center max-sm:w-full",
"text-2xl md:text-4xl px-4 md:px-6 py-6 md:py-8 flex flex-col justify-center w-full max-w-4xl mx-auto",
sans.variable,
)}
>

View file

@ -14,16 +14,18 @@ export function Welcome() {
}
return (
<div className="flex flex-col gap-4 items-center text-center">
<h1 className="text-white font-medium">Welcome to Supermemory</h1>
<p className="text-white/80 text-2xl">
<div className="flex flex-col gap-4 items-center text-center w-full">
<h1 className="text-white font-medium text-2xl md:text-4xl">
Welcome to Supermemory
</h1>
<p className="text-white/80 text-lg md:text-2xl">
We're excited to have you on board.
</p>
<button
type="button"
onClick={handleGetStarted}
className="tracking-normal w-fit flex items-center justify-center text-2xl underline cursor-pointer font-medium text-white/80 hover:text-white transition-colors"
className="tracking-normal w-fit flex items-center justify-center text-lg md:text-2xl underline cursor-pointer font-medium text-white/80 hover:text-white transition-colors"
>
Get started
<ArrowRightIcon className="size-4 ml-2" />

View file

@ -1,20 +1,44 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { generateId } from "@lib/generate-id"
import { usePersistentChat } from "@/stores/chat"
import { ArrowUp } from "lucide-react"
import { Button } from "@ui/components/button"
import { ProjectSelector } from "./project-selector"
import { ModelSelector } from "./model-selector"
import { useAuth } from "@lib/auth-context"
export function ChatInput() {
const [message, setMessage] = useState("")
const [selectedModel, setSelectedModel] = useState<
"gpt-5" | "claude-sonnet-4.5" | "gemini-2.5-pro"
>("gemini-2.5-pro")
const router = useRouter()
const { setCurrentChatId } = usePersistentChat()
const { user } = useAuth()
useEffect(() => {
const savedModel = localStorage.getItem("selectedModel") as
| "gpt-5"
| "claude-sonnet-4.5"
| "gemini-2.5-pro"
if (
savedModel &&
["gpt-5", "claude-sonnet-4.5", "gemini-2.5-pro"].includes(savedModel)
) {
setSelectedModel(savedModel)
}
}, [])
const handleModelChange = (
modelId: "gpt-5" | "claude-sonnet-4.5" | "gemini-2.5-pro",
) => {
setSelectedModel(modelId)
localStorage.setItem("selectedModel", modelId)
}
const handleSend = () => {
if (!message.trim()) return
@ -22,8 +46,8 @@ export function ChatInput() {
setCurrentChatId(newChatId)
// Store the initial message in sessionStorage for the chat page to pick up
sessionStorage.setItem(`chat-initial-${newChatId}`, message.trim())
sessionStorage.setItem(`chat-model-${newChatId}`, selectedModel)
router.push(`/chat/${newChatId}`)
@ -62,17 +86,23 @@ export function ChatInput() {
className="w-full text-foreground placeholder-muted-foreground rounded-md outline-none resize-none text-base leading-relaxed px-6 py-4 bg-transparent"
rows={2}
/>
<div className="flex items-center gap-2 w-full justify-between bg-accent py-2 px-3 rounded-b-[14px]">
<div className="flex items-center gap-2 w-full justify-between py-2 px-3 rounded-b-[14px]">
<ProjectSelector />
<Button
onClick={handleSend}
disabled={!message.trim()}
className="text-primary-foreground border-0 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed !bg-primary h-8 w-8"
variant="outline"
size="icon"
>
<ArrowUp className="size-3.5" />
</Button>
<div className="flex items-center gap-2">
<ModelSelector
selectedModel={selectedModel}
onModelChange={handleModelChange}
/>
<Button
onClick={handleSend}
disabled={!message.trim()}
className="text-primary-foreground border-0 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed !bg-primary h-8 w-8"
variant="outline"
size="icon"
>
<ArrowUp className="size-3.5" />
</Button>
</div>
</div>
</form>
</div>

View file

@ -14,17 +14,23 @@ import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import Image from "next/image"
import { analytics } from "@/lib/analytics"
import { useIsMobile } from "@hooks/use-mobile"
export function ChromeExtensionButton() {
const [isExtensionInstalled, setIsExtensionInstalled] = useState(false)
const [isChecking, setIsChecking] = useState(true)
const [isDismissed, setIsDismissed] = useState(false)
const [isMinimized, setIsMinimized] = useState(false)
const isMobile = useIsMobile()
useEffect(() => {
const dismissed =
localStorage.getItem("chrome-extension-dismissed") === "true"
const minimized =
localStorage.getItem("chrome-extension-minimized") === "true"
setIsDismissed(dismissed)
setIsMinimized(minimized)
const checkExtension = () => {
const message = { action: "check-extension" }
@ -33,9 +39,10 @@ export function ChromeExtensionButton() {
setIsExtensionInstalled(false)
setIsChecking(false)
// Auto-minimize after 3 seconds if extension is not installed and not dismissed
if (!dismissed) {
if (!dismissed && !minimized) {
setTimeout(() => {
setIsMinimized(true)
localStorage.setItem("chrome-extension-minimized", "true")
}, 3000)
}
}, 1000)
@ -77,11 +84,12 @@ export function ChromeExtensionButton() {
const handleDismiss = () => {
localStorage.setItem("chrome-extension-dismissed", "true")
localStorage.removeItem("chrome-extension-minimized")
setIsDismissed(true)
}
// Don't show if extension is installed, checking, or dismissed
if (isExtensionInstalled || isChecking || isDismissed) {
// Don't show if extension is installed, checking, dismissed, or on mobile
if (isExtensionInstalled || isChecking || isDismissed || isMobile) {
return null
}
@ -130,42 +138,42 @@ export function ChromeExtensionButton() {
<div className="px-6 py-2 pb-4 space-y-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-center flex-shrink-0">
<TwitterIcon className="fill-blue-500 text-blue-500" />
<div className="w-10 h-10 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg flex items-center justify-center flex-shrink-0">
<TwitterIcon className="fill-blue-500 dark:fill-blue-400 text-blue-500 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
<h3 className="font-semibold text-sm text-foreground">
Twitter Imports
</h3>
<p className="text-xs text-gray-600">
<p className="text-xs text-muted-foreground">
Import your twitter timeline & save tweets.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-orange-50 border border-orange-200 rounded-lg flex items-center justify-center flex-shrink-0">
<Bookmark className="w-5 h-5 text-orange-600" />
<div className="w-10 h-10 bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg flex items-center justify-center flex-shrink-0">
<Bookmark className="w-5 h-5 text-orange-600 dark:text-orange-400" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
<h3 className="font-semibold text-sm text-foreground">
Save All Bookmarks
</h3>
<p className="text-xs text-gray-600">
<p className="text-xs text-muted-foreground">
Instantly save any webpage to your memory.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-green-50 border border-green-200 rounded-lg flex items-center justify-center flex-shrink-0">
<Zap className="w-5 h-5 text-green-600" />
<div className="w-10 h-10 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg flex items-center justify-center flex-shrink-0">
<Zap className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
<h3 className="font-semibold text-sm text-foreground">
Charge Empty Memory
</h3>
<p className="text-xs text-gray-600">
<p className="text-xs text-muted-foreground">
Automatically capture & organize your browsing history.
</p>
</div>
@ -175,7 +183,7 @@ export function ChromeExtensionButton() {
<div className="px-6 pb-4">
<Button
onClick={handleInstall}
className="w-full bg-white border border-[#686CFD] text-gray-800 hover:bg-gray-50 font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
className="w-full bg-background border border-primary text-foreground hover:bg-accent font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
>
<div className="w-6 h-6 bg-[#686CFD] rounded-full flex items-center justify-center">
<Image
@ -189,7 +197,7 @@ export function ChromeExtensionButton() {
</Button>
</div>
<div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-gray-500">
<div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Users className="w-3 h-3" />
<span>4K+ users</span>

View file

@ -25,6 +25,7 @@ interface WebsiteCardProps {
description?: string
className?: string
onClick?: () => void
onOpenDetails?: () => void
onDelete?: () => void
showExternalLink?: boolean
}
@ -36,6 +37,7 @@ export const WebsiteCard = ({
description,
className,
onClick,
onOpenDetails,
onDelete,
showExternalLink = true,
}: WebsiteCardProps) => {
@ -44,6 +46,8 @@ export const WebsiteCard = ({
const handleCardClick = () => {
if (onClick) {
onClick()
} else if (onOpenDetails) {
onOpenDetails()
} else {
window.open(url, "_blank", "noopener,noreferrer")
}

View file

@ -10,7 +10,6 @@ import {
DialogTitle,
} from "@repo/ui/components/dialog"
import { Input } from "@repo/ui/components/input"
import { Label } from "@repo/ui/components/label"
import { Loader2 } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { useState } from "react"
@ -47,7 +46,7 @@ export function CreateProjectDialog({
<AnimatePresence>
{open && (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="sm:max-w-2xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
<DialogContent className="sm:max-w-2xl backdrop-blur-xl">
<motion.div
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
@ -55,7 +54,7 @@ export function CreateProjectDialog({
>
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription className="text-white/60">
<DialogDescription>
Give your project a unique name
</DialogDescription>
</DialogHeader>
@ -66,9 +65,7 @@ export function CreateProjectDialog({
initial={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 }}
>
<Label htmlFor="projectName">Project Name</Label>
<Input
className="bg-white/5 border-white/10 text-white"
id="projectName"
onChange={(e) => setProjectName(e.target.value)}
onKeyDown={(e) => {
@ -79,47 +76,31 @@ export function CreateProjectDialog({
placeholder="My Awesome Project"
value={projectName}
/>
<p className="text-xs text-white/50">
<p className="text-xs">
This will help you organize your memories
</p>
</motion.div>
</div>
<DialogFooter>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
<Button onClick={handleClose} type="button" variant="outline">
Cancel
</Button>
<Button
disabled={
createProjectMutation.isPending || !projectName.trim()
}
onClick={handleCreate}
type="button"
>
<Button
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={handleClose}
type="button"
variant="outline"
>
Cancel
</Button>
</motion.div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
className="bg-white/10 hover:bg-white/20 text-white border-white/20"
disabled={
createProjectMutation.isPending || !projectName.trim()
}
onClick={handleCreate}
type="button"
>
{createProjectMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Creating...
</>
) : (
"Create Project"
)}
</Button>
</motion.div>
{createProjectMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Creating...
</>
) : (
"Create Project"
)}
</Button>
</DialogFooter>
</motion.div>
</DialogContent>

View file

@ -6,11 +6,14 @@ import {
Plus,
SunIcon,
MonitorIcon,
Network,
User,
CreditCard,
Chrome,
LogOut,
WaypointsIcon,
Gauge,
HistoryIcon,
Trash2,
} from "lucide-react"
import {
DropdownMenuContent,
@ -21,21 +24,61 @@ import {
import { DropdownMenuItem } from "@ui/components/dropdown-menu"
import { DropdownMenu } from "@ui/components/dropdown-menu"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
import { useAuth } from "@lib/auth-context"
import { ConnectAIModal } from "./connect-ai-modal"
import { useTheme } from "next-themes"
import { cn } from "@lib/utils"
import { useRouter } from "next/navigation"
import { usePathname, useRouter } from "next/navigation"
import { MCPIcon } from "./menu"
import { authClient } from "@lib/auth"
import { analytics } from "@/lib/analytics"
import { useGraphModal } from "@/stores"
import { useGraphModal, usePersistentChat, useProject } from "@/stores"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ui/components/dialog"
import { ScrollArea } from "@ui/components/scroll-area"
import { formatDistanceToNow } from "date-fns"
import { cn } from "@lib/utils"
import { useMemo, useState } from "react"
export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
const { user } = useAuth()
const { theme, setTheme } = useTheme()
const router = useRouter()
const { setIsOpen: setGraphModalOpen } = useGraphModal()
const {
getCurrentChat,
conversations,
currentChatId,
setCurrentChatId,
deleteConversation,
} = usePersistentChat()
const { selectedProject } = useProject()
const pathname = usePathname()
const [isDialogOpen, setIsDialogOpen] = useState(false)
const sorted = useMemo(() => {
return [...conversations].sort((a, b) =>
a.lastUpdated < b.lastUpdated ? 1 : -1,
)
}, [conversations])
function handleNewChat() {
analytics.newChatStarted()
const newId = crypto.randomUUID()
setCurrentChatId(newId)
router.push(`/chat/${newId}`)
setIsDialogOpen(false)
}
function formatRelativeTime(isoString: string): string {
return formatDistanceToNow(new Date(isoString), { addSuffix: true })
}
const handleSignOut = () => {
analytics.userSignedOut()
@ -46,20 +89,31 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
return (
<div className="flex items-center justify-between w-full p-3 md:p-4">
<div className="flex items-center gap-2 md:gap-3 justify-between w-full">
<Link
className="pointer-events-auto"
href={
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://app.supermemory.ai"
}
rel="noopener noreferrer"
>
<LogoFull className="h-8 hidden md:block" />
<Logo className="h-8 md:hidden" />
</Link>
<div className="flex items-center gap-1.5 md:gap-2">
<Link
className="pointer-events-auto"
href={
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://app.supermemory.ai"
}
rel="noopener noreferrer"
>
{getCurrentChat()?.title && pathname.includes("/chat") ? (
<div className="flex items-center gap-4">
<Logo className="h-6 block text-foreground" />
<span className="truncate">{getCurrentChat()?.title}</span>
</div>
) : (
<>
<LogoFull className="h-8 hidden md:block" />
<Logo className="h-8 md:hidden text-foreground" />
</>
)}
</Link>
</div>
<div className="flex items-center gap-1.5 md:gap-3">
<div className="flex items-center gap-1.5 md:gap-2">
<Button
variant="secondary"
size="sm"
@ -72,20 +126,131 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
c
</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setGraphModalOpen(true)}
className="gap-1.5"
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
setIsDialogOpen(open)
if (open) {
analytics.chatHistoryViewed()
}
}}
>
<Network className="h-4 w-4" />
<span className="hidden sm:inline">Graph View</span>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant="ghost" size="sm">
<HistoryIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Chat History</p>
</TooltipContent>
</Tooltip>
<DialogContent className="sm:max-w-lg">
<DialogHeader className="pb-4 border-b rounded-t-lg">
<DialogTitle className="">Conversations</DialogTitle>
<DialogDescription>
Project{" "}
<span className="font-mono font-medium">
{selectedProject}
</span>
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="flex flex-col gap-1">
{sorted.map((c) => {
const isActive = c.id === currentChatId
return (
<button
key={c.id}
type="button"
onClick={() => {
setCurrentChatId(c.id)
router.push(`/chat/${c.id}`)
setIsDialogOpen(false)
}}
className={cn(
"flex items-center justify-between rounded-md px-3 py-2 outline-none w-full text-left",
"transition-colors",
isActive ? "bg-primary/10" : "hover:bg-muted",
)}
aria-current={isActive ? "true" : undefined}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span
className={cn(
"text-sm font-medium truncate",
isActive ? "text-foreground" : undefined,
)}
>
{c.title || "Untitled Chat"}
</span>
</div>
<div className="text-xs text-muted-foreground">
Last updated {formatRelativeTime(c.lastUpdated)}
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
analytics.chatDeleted()
deleteConversation(c.id)
}}
aria-label="Delete conversation"
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</button>
)
})}
{sorted.length === 0 && (
<div className="text-xs text-muted-foreground px-3 py-2">
No conversations yet
</div>
)}
</div>
</ScrollArea>
<Button
variant="outline"
size="lg"
className="w-full border-dashed"
onClick={handleNewChat}
>
<Plus className="size-4 mr-1" /> New Conversation
</Button>
</DialogContent>
</Dialog>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => setGraphModalOpen(true)}
>
<WaypointsIcon className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Graph View</p>
</TooltipContent>
</Tooltip>
<ConnectAIModal>
<Button variant="ghost" size="sm" className="gap-1.5">
<MCPIcon className="h-4 w-4" />
<span className="hidden lg:inline">Connect to AI (MCP)</span>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" className="gap-1.5">
<MCPIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Connect to AI (MCP)</p>
</TooltipContent>
</Tooltip>
</ConnectAIModal>
<DropdownMenu>
<DropdownMenuTrigger>
@ -103,15 +268,21 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/settings")}>
<User className="h-4 w-4 mr-2" />
<User className="h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => router.push("/settings/billing")}
>
<CreditCard className="h-4 w-4 mr-2" />
<CreditCard className="h-4 w-4" />
Billing
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => router.push("/settings/integrations")}
>
<Gauge className="h-4 w-4" />
Integrations
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
window.open(
@ -121,7 +292,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
)
}}
>
<Chrome className="h-4 w-4 mr-2" />
<Chrome className="h-4 w-4" />
Chrome Extension
</DropdownMenuItem>
<DropdownMenuItem
@ -142,7 +313,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
<MonitorIcon
className={cn(
theme === "system"
? "text-primay-foreground"
? "text-primary-foreground"
: "text-muted-foreground",
"h-3 w-3 group-hover:text-foreground",
)}
@ -160,7 +331,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
<SunIcon
className={cn(
theme === "light"
? "text-primay-foreground"
? "text-primary-foreground"
: "text-muted-foreground",
"h-3 w-3 group-hover:text-foreground",
)}
@ -178,7 +349,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
<MoonIcon
className={cn(
theme === "dark"
? "text-primay-foreground"
? "text-primary-foreground"
: "text-muted-foreground",
"h-3 w-3 group-hover:text-foreground",
)}
@ -188,7 +359,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleSignOut()}>
<LogOut className="h-4 w-4 mr-2" />
<LogOut className="h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -89,6 +89,7 @@ const DocumentCard = memo(
url={document.url}
title={document.title || "Untitled Document"}
image={document.ogImage}
onOpenDetails={() => onOpenDetails(document)}
onDelete={() => onDelete(document)}
/>
)

View file

@ -28,6 +28,7 @@ import type { z } from "zod"
import { formatDate, getSourceUrl } from "."
import { Label1Regular } from "@ui/text/label/label-1-regular"
import { HTMLContentRenderer } from "./html-content-renderer"
import { Button } from "@ui/components/button"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
@ -141,11 +142,13 @@ export const MemoryDetail = memo(
TitleComponent: typeof DialogTitle | typeof DrawerTitle
}) => (
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-2 md:gap-3 flex-1 min-w-0">
<div className="flex items-end gap-2 md:gap-3 flex-1 min-w-0">
<div className="p-1.5 md:p-2 rounded-lg bg-muted/10 flex-shrink-0">
{getDocumentIcon(
document.type,
"w-4 h-4 md:w-5 md:h-5 text-foreground",
document.source ?? undefined,
document.url ?? undefined,
)}
</div>
<div className="flex-1 min-w-0">
@ -156,25 +159,24 @@ export const MemoryDetail = memo(
<span>{formatDocumentType(document.type)}</span>
<span></span>
<span>{formatDate(document.createdAt)}</span>
{document.url && (
<>
<span></span>
<button
className="flex items-center gap-0.5 md:gap-1 transition-all hover:gap-1 md:hover:gap-2 text-primary hover:text-primary/80 whitespace-nowrap"
onClick={() => {
const sourceUrl = getSourceUrl(document)
window.open(sourceUrl ?? undefined, "_blank")
}}
type="button"
>
<span className="hidden sm:inline">View source</span>
<span className="sm:hidden">Source</span>
<ExternalLink className="w-2.5 h-2.5 md:w-3 md:h-3" />
</button>
</>
)}
</div>
</div>
{document.url && (
<div className="flex items-end">
<Button
onClick={() => {
const sourceUrl = getSourceUrl(document)
window.open(sourceUrl ?? undefined, "_blank")
}}
variant="secondary"
size="sm"
>
<span className="hidden sm:inline">visit source</span>
<span className="sm:hidden">Source</span>
<ExternalLink className="w-2.5 h-2.5 md:w-3 md:h-3" />
</Button>
</div>
)}
</div>
</div>
)
@ -227,10 +229,10 @@ export const MemoryDetail = memo(
<div className="space-y-6">
{activeMemories.length > 0 && (
<div>
<div className="text-sm font-medium mb-2 flex items-start gap-2 py-2 text-muted-foreground">
<div className="text-sm font-medium flex items-start gap-2 pb-2 text-muted-foreground">
Active Memories ({activeMemories.length})
</div>
<div className="space-y-3">
<div className="space-y-3 max-h-[80vh] overflow-y-auto custom-scrollbar">
{activeMemories.map((memory) => (
<div key={memory.id}>
<MemoryDetailItem memory={memory} />
@ -281,7 +283,7 @@ export const MemoryDetail = memo(
Content
</h4>
</div>
<div className="p-3 md:p-4">
<div className="p-3 md:p-4 m-4">
<ContentDisplaySection />
</div>
</div>
@ -324,8 +326,8 @@ export const MemoryDetail = memo(
<div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
<div className="flex-1 flex flex-col h-full justify-between min-w-0">
<div className="p-2 px-3 md:px-4 overflow-y-auto custom-scrollbar transition-all duration-300">
<h3 className="font-medium text-[10px] md:text-xs text-muted-foreground uppercase pb-1 px-1">
<div className="p-2 px-3 md:pl-4 overflow-y-auto custom-scrollbar transition-all duration-300">
<h3 className="font-medium text-[10px] md:text-sm text-muted-foreground pb-2 px-1">
Content
</h3>
<ContentDisplaySection />
@ -370,9 +372,9 @@ export const MemoryDetail = memo(
</div>
</div>
<div className="w-full lg:w-96 flex flex-col border-t lg:border-t-0 lg:border-l border-border">
<div className="w-full lg:w-96 flex flex-col border-t lg:border-t-0 border-border">
<div className="flex-1 flex flex-col">
<div className="flex-1 memory-dialog-scroll overflow-y-auto p-3 md:p-4">
<div className="flex-1 memory-dialog-scroll overflow-y-auto p-2 md:p-3">
<MemoryContent />
</div>
</div>

View file

@ -294,10 +294,7 @@ export const MemoryListView = ({
// Always render with consistent structure
return (
<>
<div
className="h-full overflow-hidden relative pb-20"
ref={containerRef}
>
<div className="h-full overflow-hidden relative pb-20" ref={containerRef}>
{error ? (
<div className="h-full flex items-center justify-center p-4">
<div className="rounded-xl overflow-hidden">

View file

@ -0,0 +1,146 @@
"use client"
import { useState } from "react"
import { Button } from "@repo/ui/components/button"
import { ChevronDown } from "lucide-react"
import { motion } from "motion/react"
const models = [
{
id: "gpt-5",
name: "GPT 5",
description: "OpenAI's latest model",
},
{
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
description: "Anthropic's advanced model",
},
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
description: "Google's most capable model",
},
] as const
type ModelId = (typeof models)[number]["id"]
interface ModelSelectorProps {
selectedModel?: ModelId
onModelChange?: (modelId: ModelId) => void
disabled?: boolean
}
export function ModelSelector({
selectedModel = "gemini-2.5-pro",
onModelChange,
disabled = false,
}: ModelSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const currentModel = models.find((m) => m.id === selectedModel) || models[0]
const handleModelSelect = (modelId: ModelId) => {
onModelChange?.(modelId)
setIsOpen(false)
}
return (
<div className="relative">
<Button
variant="ghost"
className="flex items-center gap-1.5 px-2 py-1.5 rounded-md transition-colors"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<title>Model Selector</title>
<g clip-path="url(#clip0_4418_9868)">
<path
d="M12.92 2.25984L19.43 5.76984C20.19 6.17984 20.19 7.34984 19.43 7.75984L12.92 11.2698C12.34 11.5798 11.66 11.5798 11.08 11.2698L4.57 7.75984C3.81 7.34984 3.81 6.17984 4.57 5.76984L11.08 2.25984C11.66 1.94984 12.34 1.94984 12.92 2.25984Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3.61 10.1297L9.66 13.1597C10.41 13.5397 10.89 14.3097 10.89 15.1497V20.8697C10.89 21.6997 10.02 22.2297 9.28 21.8597L3.23 18.8297C2.48 18.4497 2 17.6797 2 16.8397V11.1197C2 10.2897 2.87 9.75968 3.61 10.1297Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20.39 10.1297L14.34 13.1597C13.59 13.5397 13.11 14.3097 13.11 15.1497V20.8697C13.11 21.6997 13.98 22.2297 14.72 21.8597L20.77 18.8297C21.52 18.4497 22 17.6797 22 16.8397V11.1197C22 10.2897 21.13 9.75968 20.39 10.1297Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_4418_9868">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
<span className="text-xs font-medium max-w-32 truncate">
{currentModel.name}
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.25 }}
>
<ChevronDown className="h-3 w-3" />
</motion.div>
</Button>
{isOpen && (
<>
<button
type="button"
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
onKeyDown={(e) => e.key === "Escape" && setIsOpen(false)}
aria-label="Close model selector"
/>
<div className="absolute top-full left-0 mt-1 w-64 bg-background/95 backdrop-blur-xl border border-border rounded-md shadow-xl z-50 overflow-hidden space-y-1">
<div className="p-1.5 space-y-1">
{models.map((model) => (
<button
key={model.id}
type="button"
className={`flex items-center p-1 px-2 rounded-md transition-colors cursor-pointer w-full text-left ${
selectedModel === model.id
? "bg-accent"
: "hover:bg-accent/50"
}`}
onClick={() => handleModelSelect(model.id)}
onKeyDown={(e) =>
e.key === "Enter" && handleModelSelect(model.id)
}
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">
{model.name}
</div>
<div className="text-xs text-muted-foreground truncate">
{model.description}
</div>
</div>
</button>
))}
</div>
</div>
</>
)}
</div>
)
}

View file

@ -25,7 +25,7 @@ import {
SelectTrigger,
SelectValue,
} from "@repo/ui/components/select"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query"
import {
ChevronDown,
FolderIcon,
@ -36,7 +36,6 @@ import {
} from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { useState } from "react"
import { toast } from "sonner"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { useProjectName } from "@/hooks/use-project-name"
import { useProject } from "@/stores"
@ -52,7 +51,6 @@ interface Project {
}
export function ProjectSelector() {
const queryClient = useQueryClient()
const [isOpen, setIsOpen] = useState(false)
const [showCreateDialog, setShowCreateDialog] = useState(false)
const { selectedProject } = useProject()
@ -69,13 +67,6 @@ export function ProjectSelector() {
action: "move",
targetProjectId: DEFAULT_PROJECT_ID,
})
const [expDialog, setExpDialog] = useState<{
open: boolean
projectId: string
}>({
open: false,
projectId: "",
})
const { data: projects = [], isLoading } = useQuery({
queryKey: ["projects"],
@ -91,30 +82,6 @@ export function ProjectSelector() {
staleTime: 30 * 1000,
})
const enableExperimentalMutation = useMutation({
mutationFn: async (projectId: string) => {
const response = await $fetch(
`@post/projects/${projectId}/enable-experimental`,
)
if (response.error) {
throw new Error(
response.error?.message || "Failed to enable experimental mode",
)
}
return response.data
},
onSuccess: () => {
toast.success("Experimental mode enabled for project")
queryClient.invalidateQueries({ queryKey: ["projects"] })
setExpDialog({ open: false, projectId: "" })
},
onError: (error) => {
toast.error("Failed to enable experimental mode", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
const handleProjectSelect = (containerTag: string) => {
switchProject(containerTag)
setIsOpen(false)
@ -138,7 +105,7 @@ export function ProjectSelector() {
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.15 }}
transition={{ duration: 0.25 }}
>
<ChevronDown className="h-3 w-3" />
</motion.div>
@ -165,33 +132,30 @@ export function ProjectSelector() {
<div className="p-1.5 max-h-64 overflow-y-auto">
<Button
variant="ghost"
className={`flex items-center justify-between p-2 rounded-md transition-colors cursor-pointer ${
className={`flex items-center w-full justify-between p-2 rounded-md transition-colors cursor-pointer ${
selectedProject === DEFAULT_PROJECT_ID
? "bg-accent"
: "hover:bg-accent/50"
: "hover:bg-accent/20"
}`}
onClick={() => handleProjectSelect(DEFAULT_PROJECT_ID)}
>
<div className="flex items-center gap-2">
<FolderIcon className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Default</span>
<span className="text-xs font-medium">Default Project</span>
</div>
</Button>
{/* User Projects */}
{projects
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
.map((project: Project, index: number) => (
<motion.div
.map((project: Project) => (
<div
key={project.id}
className={`flex items-center justify-between p-2 rounded-md transition-colors group ${
selectedProject === project.containerTag
? "bg-accent"
: "hover:bg-accent/50"
}`}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.03 }}
>
<button
className="flex items-center gap-2 flex-1 cursor-pointer"
@ -208,45 +172,18 @@ export function ProjectSelector() {
<div className="flex items-center gap-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<motion.button
className="p-1 hover:bg-accent rounded transition-all"
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={(e) => e.stopPropagation()}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<MoreHorizontal className="h-3 w-3" />
</motion.button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Show experimental toggle only if NOT experimental and NOT default project */}
{!project.isExperimental &&
project.containerTag !== DEFAULT_PROJECT_ID && (
<DropdownMenuItem
className="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 cursor-pointer text-xs"
onClick={(e) => {
e.stopPropagation()
setExpDialog({
open: true,
projectId: project.id,
})
setIsOpen(false)
}}
>
<div className="h-3 w-3 mr-2 rounded border border-blue-600 dark:border-blue-400" />
Enable Experimental Mode
</DropdownMenuItem>
)}
{project.isExperimental && (
<DropdownMenuItem
className="text-blue-600/50 dark:text-blue-300/50 text-xs"
disabled
>
<div className="h-3 w-3 mr-2 rounded bg-blue-600 dark:bg-blue-400" />
Experimental Mode Active
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-red-600 dark:text-red-400 hover:text-red-500 dark:hover:text-red-300 cursor-pointer text-xs"
className="cursor-pointer text-xs hover:text-red-500"
onClick={(e) => {
e.stopPropagation()
setDeleteDialog({
@ -262,13 +199,13 @@ export function ProjectSelector() {
setIsOpen(false)
}}
>
<Trash2 className="h-3 w-3 mr-2" />
<Trash2 className="h-3 w-3" />
Delete Project
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</motion.div>
</div>
))}
<motion.div
@ -489,82 +426,6 @@ export function ProjectSelector() {
</Dialog>
)}
</AnimatePresence>
{/* Experimental Mode Confirmation Dialog */}
<AnimatePresence>
{expDialog.open && (
<Dialog
onOpenChange={(open) => setExpDialog({ ...expDialog, open })}
open={expDialog.open}
>
<DialogContent className="sm:max-w-lg">
<motion.div
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col gap-4"
exit={{ opacity: 0, scale: 0.95 }}
initial={{ opacity: 0, scale: 0.95 }}
>
<DialogHeader>
<DialogTitle>Enable Experimental Mode?</DialogTitle>
<DialogDescription>
Experimental mode enables beta features and advanced memory
relationships for this project.
<br />
<br />
<span className="text-yellow-600 dark:text-yellow-400 font-medium">
Warning:
</span>{" "}
This action is{" "}
<span className="text-red-600 dark:text-red-400 font-bold">
irreversible
</span>
. Once enabled, you cannot return to regular mode for this
project.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
onClick={() =>
setExpDialog({ open: false, projectId: "" })
}
type="button"
variant="outline"
>
Cancel
</Button>
</motion.div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white"
disabled={enableExperimentalMutation.isPending}
onClick={() =>
enableExperimentalMutation.mutate(expDialog.projectId)
}
type="button"
>
{enableExperimentalMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Enabling...
</>
) : (
"Enable Experimental Mode"
)}
</Button>
</motion.div>
</DialogFooter>
</motion.div>
</DialogContent>
</Dialog>
)}
</AnimatePresence>
</div>
)
}

View file

@ -1021,7 +1021,6 @@ export function AddMemoryView({
>
<Label htmlFor="projectName">Project Name</Label>
<Input
className="bg-white/5 border-white/10"
id="projectName"
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="My Awesome Project"

View file

@ -10,7 +10,6 @@ import {
ChevronDown,
ChevronRight,
Copy,
Plus,
RotateCcw,
X,
} from "lucide-react"
@ -216,7 +215,19 @@ export function ChatMessages() {
getCurrentChat,
} = usePersistentChat()
const storageKey = `chat-model-${currentChatId}`
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<
"gpt-5" | "claude-sonnet-4.5" | "gemini-2.5-pro"
>(
(sessionStorage.getItem(storageKey) as
| "gpt-5"
| "claude-sonnet-4.5"
| "gemini-2.5-pro") ||
"gemini-2.5-pro" ||
"gemini-2.5-pro",
)
const activeChatIdRef = useRef<string | null>(null)
const shouldGenerateTitleRef = useRef<boolean>(false)
const hasRunInitialMessageRef = useRef<boolean>(false)
@ -229,7 +240,12 @@ export function ChatMessages() {
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
credentials: "include",
body: { metadata: { projectId: selectedProject } },
body: {
metadata: {
projectId: selectedProject,
model: selectedModel,
},
},
}),
maxSteps: 2,
onFinish: (result) => {
@ -254,6 +270,22 @@ export function ChatMessages() {
activeChatIdRef.current = currentChatId ?? id ?? null
}, [currentChatId, id])
useEffect(() => {
if (currentChatId) {
const savedModel = sessionStorage.getItem(storageKey) as
| "gpt-5"
| "claude-sonnet-4.5"
| "gemini-2.5-pro"
if (
savedModel &&
["gpt-5", "claude-sonnet-4.5", "gemini-2.5-pro"].includes(savedModel)
) {
setSelectedModel(savedModel)
}
}
}, [currentChatId])
useEffect(() => {
if (currentChatId && !hasRunInitialMessageRef.current) {
// Check if there's an initial message from the home page in sessionStorage

View file

@ -301,7 +301,7 @@ export function ProfileView() {
</div>
</h4>
<ul className="space-y-2">
<li className="flex items-center gap-2 text-sm text-black/90">
<li className="flex items-center gap-2 text-xs sm:text-sm text-foreground">
<CheckCircle className="h-4 w-4 text-green-400" />
Unlimited memories
</li>

View file

@ -1,59 +1,119 @@
import { MCPIcon } from '@/components/menu';
import { colors } from '@repo/ui/memory-graph/constants';
import { MCPIcon } from "@/components/menu"
import { colors } from "@repo/ui/memory-graph/constants"
import {
GoogleDocs,
MicrosoftWord,
NotionDoc,
GoogleDrive,
GoogleSheets,
GoogleSlides,
PDF,
OneDrive,
MicrosoftOneNote,
MicrosoftPowerpoint,
MicrosoftExcel,
} from '@ui/assets/icons';
import { FileText } from 'lucide-react';
GoogleDocs,
MicrosoftWord,
NotionDoc,
GoogleDrive,
GoogleSheets,
GoogleSlides,
PDF,
OneDrive,
MicrosoftOneNote,
MicrosoftPowerpoint,
MicrosoftExcel,
} from "@ui/assets/icons"
import { FileText, Globe } from "lucide-react"
import { useState } from "react"
export const getDocumentIcon = (type: string, className: string, source?: string) => {
const iconProps = {
className,
style: { color: colors.text.muted },
};
const getFaviconUrl = (url: string): string => {
try {
const domain = new URL(url).hostname
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
} catch {
return ""
}
}
if(source === "mcp") {
return <MCPIcon {...iconProps} />;
}
const FaviconIcon = ({
url,
className,
iconProps,
}: {
url: string
className: string
iconProps: { className: string; style: { color: string } }
}) => {
const [hasError, setHasError] = useState(false)
const faviconUrl = getFaviconUrl(url)
switch (type) {
case 'google_doc':
return <GoogleDocs {...iconProps} />;
case 'google_sheet':
return <GoogleSheets {...iconProps} />;
case 'google_slide':
return <GoogleSlides {...iconProps} />;
case 'google_drive':
return <GoogleDrive {...iconProps} />;
case 'notion':
case 'notion_doc':
return <NotionDoc {...iconProps} />;
case 'word':
case 'microsoft_word':
return <MicrosoftWord {...iconProps} />;
case 'excel':
case 'microsoft_excel':
return <MicrosoftExcel {...iconProps} />;
case 'powerpoint':
case 'microsoft_powerpoint':
return <MicrosoftPowerpoint {...iconProps} />;
case 'onenote':
case 'microsoft_onenote':
return <MicrosoftOneNote {...iconProps} />;
case 'onedrive':
return <OneDrive {...iconProps} />;
case 'pdf':
return <PDF {...iconProps} />;
default:
return <FileText {...iconProps} />;
}
};
if (hasError || !faviconUrl) {
return <Globe {...iconProps} />
}
return (
<img
src={faviconUrl}
alt="Website favicon"
className={className}
style={{
width: "2em",
height: "2em",
objectFit: "contain",
}}
onError={() => setHasError(true)}
/>
)
}
export const getDocumentIcon = (
type: string,
className: string,
source?: string,
url?: string,
) => {
const iconProps = {
className,
style: { color: colors.text.muted },
}
if (source === "mcp") {
return <MCPIcon {...iconProps} />
}
if (
type === "webpage" ||
type === "url" ||
(url && (type === "unknown" || !type))
) {
if (url) {
return (
<FaviconIcon url={url} className={className} iconProps={iconProps} />
)
}
return <Globe {...iconProps} />
}
switch (type) {
case "google_doc":
return <GoogleDocs {...iconProps} />
case "google_sheet":
return <GoogleSheets {...iconProps} />
case "google_slide":
return <GoogleSlides {...iconProps} />
case "google_drive":
return <GoogleDrive {...iconProps} />
case "notion":
case "notion_doc":
return <NotionDoc {...iconProps} />
case "word":
case "microsoft_word":
return <MicrosoftWord {...iconProps} />
case "excel":
case "microsoft_excel":
return <MicrosoftExcel {...iconProps} />
case "powerpoint":
case "microsoft_powerpoint":
return <MicrosoftPowerpoint {...iconProps} />
case "onenote":
case "microsoft_onenote":
return <MicrosoftOneNote {...iconProps} />
case "onedrive":
return <OneDrive {...iconProps} />
case "pdf":
return <PDF {...iconProps} />
default:
return <FileText {...iconProps} />
}
}

View file

@ -16,7 +16,7 @@ export const Logo = ({
<title>supermemory Logo</title>
<path
d="M205.864 66.263h-76.401V0h-24.684v71.897c0 7.636 3.021 14.97 8.391 20.373l62.383 62.777 17.454-17.564-46.076-46.365h58.948v-24.84l-.015-.015ZM12.872 30.517l46.075 46.365H0v24.84h76.4v66.264h24.685V96.089c0-7.637-3.021-14.97-8.39-20.374l-62.37-62.762-17.453 17.564Z"
fill="#EFEFEF"
fill="currentColor"
/>
</svg>
);

View file

@ -43,7 +43,7 @@ function TooltipContent({
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
"bg-secondary text-secondary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
data-slot="tooltip-content"
@ -51,7 +51,7 @@ function TooltipContent({
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-sm" />
<TooltipPrimitive.Arrow className="bg-secondary fill-secondary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-sm" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);

View file

@ -65,7 +65,7 @@
--destructive: oklch(0.6368 0.2078 25.3313);
--border: oklch(0.9366 0.0017 247.8401);
--input: oklch(0.9702 0 0);
--ring: oklch(1 0 0);
--ring: oklch(0 0 0);
--chart-1: oklch(0.7197 0.1448 266.6983);
--chart-2: oklch(0.8101 0.1488 148.2032);
--chart-3: oklch(0.7507 0.1353 58.4182);
@ -125,7 +125,7 @@
--destructive: oklch(0.6368 0.2078 25.3313);
--border: oklch(0.3715 0 0);
--input: oklch(0.262 0.0074 285.8781);
--ring: oklch(0 0 0);
--ring: oklch(1 0 0);
--chart-1: oklch(0.7197 0.1448 266.6983);
--chart-2: oklch(0.8101 0.1488 148.2032);
--chart-3: oklch(0.7507 0.1353 58.4182);