Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 142 additions & 3 deletions apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
LogOut,
WaypointsIcon,
Gauge,
HistoryIcon,
Trash2,
} from "lucide-react"
import {
DropdownMenuContent,
Expand All @@ -26,20 +28,57 @@ 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 { usePathname, useRouter } from "next/navigation"
import { MCPIcon } from "./menu"
import { authClient } from "@lib/auth"
import { analytics } from "@/lib/analytics"
import { useGraphModal, usePersistentChat } 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 } = usePersistentChat()
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()
Expand Down Expand Up @@ -87,6 +126,106 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
c
</span>
</Button>
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
setIsDialogOpen(open)
if (open) {
analytics.chatHistoryViewed()
}
}}
>
<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
Expand Down
13 changes: 8 additions & 5 deletions apps/web/components/views/chat/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ChevronDown,
ChevronRight,
Copy,
Plus,
RotateCcw,
X,
} from "lucide-react"
Expand Down Expand Up @@ -221,15 +220,20 @@ export function ChatMessages() {
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")
>(
(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,
Expand Down Expand Up @@ -277,7 +281,6 @@ export function ChatMessages() {
savedModel &&
["gpt-5", "claude-sonnet-4.5", "gemini-2.5-pro"].includes(savedModel)
) {
console.log("savedModel", savedModel)
setSelectedModel(savedModel)
}
}
Expand Down