feat: multiple models & ui improvements (#455)

This commit is contained in:
MaheshtheDev 2025-10-06 16:25:38 +00:00
parent cdfb942fa8
commit e86104b645
8 changed files with 253 additions and 29 deletions

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

@ -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}`)
@ -64,15 +88,21 @@ export function ChatInput() {
/>
<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

@ -26,7 +26,11 @@ export function ChromeExtensionButton() {
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" }
@ -35,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)
@ -79,6 +84,7 @@ export function ChromeExtensionButton() {
const handleDismiss = () => {
localStorage.setItem("chrome-extension-dismissed", "true")
localStorage.removeItem("chrome-extension-minimized")
setIsDismissed(true)
}

View file

@ -22,6 +22,7 @@ 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"
@ -86,19 +87,31 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
c
</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setGraphModalOpen(true)}
>
<WaypointsIcon className="h-5 w-5" />
{/*<span className="hidden md:inline">Graph View</span>*/}
</Button>
<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>

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

@ -216,20 +216,32 @@ 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)
const { setDocumentIds } = useGraphHighlights()
console.log("selectedModel", selectedModel)
const { messages, sendMessage, status, stop, setMessages, id, regenerate } =
useChat({
id: currentChatId ?? undefined,
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 +266,23 @@ 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)
) {
console.log("savedModel", 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

@ -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>
);