mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
Merge branch 'main' of https://github.com/supermemoryai/supermemory
This commit is contained in:
commit
290f731b51
27 changed files with 1515 additions and 1044 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const DocumentCard = memo(
|
|||
url={document.url}
|
||||
title={document.title || "Untitled Document"}
|
||||
image={document.ogImage}
|
||||
onOpenDetails={() => onOpenDetails(document)}
|
||||
onDelete={() => onDelete(document)}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
146
apps/web/components/model-selector.tsx
Normal file
146
apps/web/components/model-selector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue