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
30 changes: 16 additions & 14 deletions apps/web/app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function NewPage() {

return (
<HotkeysProvider>
<div className="h-screen overflow-hidden bg-black">
<div className="bg-black">
<AnimatedGradientBackground
topPosition="15%"
animateFromBottom={false}
Expand All @@ -28,21 +28,23 @@ export default function NewPage() {
onAddMemory={() => setIsAddDocumentOpen(true)}
onOpenMCP={() => setIsMCPModalOpen(true)}
/>
<main className="relative">
<div key={`main-container-${isChatOpen}`} className="relative z-10">
<div className="flex flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col justify-start p-6 pr-0">
<MemoriesGrid isChatOpen={isChatOpen} />
</div>
<AnimatePresence mode="popLayout">
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
/>
</AnimatePresence>
</div>
<main
key={`main-container-${isChatOpen}`}
className="z-10 flex flex-row relative"
>
<div className="flex-1 p-6 pr-0">
<MemoriesGrid isChatOpen={isChatOpen} />
</div>
<div className="sticky top-0 h-screen">
<AnimatePresence mode="popLayout">
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
/>
</AnimatePresence>
</div>
</main>

<AddDocumentModal
isOpen={isAddDocumentOpen}
onClose={() => setIsAddDocumentOpen(false)}
Expand Down
115 changes: 8 additions & 107 deletions apps/web/components/new/add-document/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,21 @@
"use client"

import { useState, useEffect, useMemo, useCallback } from "react"
import { useState, useEffect, useCallback } from "react"
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import {
FileTextIcon,
GlobeIcon,
ZapIcon,
ChevronsUpDownIcon,
FolderIcon,
Loader2,
} from "lucide-react"
import { FileTextIcon, GlobeIcon, ZapIcon, Loader2 } from "lucide-react"
import { Button } from "@ui/components/button"
import { ConnectContent } from "./connections"
import { NoteContent } from "./note"
import { LinkContent, type LinkData } from "./link"
import { FileContent, type FileData } from "./file"
import { useProject } from "@/stores"
import { $fetch } from "@lib/api"
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
import type { Project } from "@repo/lib/types"
import { useQuery } from "@tanstack/react-query"
import { motion } from "motion/react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@repo/ui/components/dropdown-menu"
import { toast } from "sonner"
import { useDocumentMutations } from "../../../hooks/use-document-mutations"
import { useCustomer } from "autumn-js/react"
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
import { SpaceSelector } from "../space-selector"

type TabType = "note" | "link" | "file" | "connect"

Expand Down Expand Up @@ -115,7 +98,6 @@ export function AddDocument({
const [localSelectedProject, setLocalSelectedProject] = useState<string>(
globalSelectedProject,
)
const [isProjectSelectorOpen, setIsProjectSelectorOpen] = useState(false)

// Form data state for button click handling
const [noteContent, setNoteContent] = useState("")
Expand Down Expand Up @@ -147,33 +129,6 @@ export function AddDocument({
setLocalSelectedProject(globalSelectedProject)
}, [globalSelectedProject])

const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
const response = await $fetch("@get/projects")

if (response.error) {
throw new Error(response.error?.message || "Failed to load projects")
}

return response.data?.projects || []
},
staleTime: 30 * 1000,
})

const projectName = useMemo(() => {
if (localSelectedProject === DEFAULT_PROJECT_ID) return "Default Project"
const found = projects.find(
(p: Project) => p.containerTag === localSelectedProject,
)
return found?.name ?? localSelectedProject
}, [projects, localSelectedProject])

const handleProjectSelect = (containerTag: string) => {
setLocalSelectedProject(containerTag)
setIsProjectSelectorOpen(false)
}

useEffect(() => {
if (defaultTab) {
setActiveTab(defaultTab)
Expand Down Expand Up @@ -334,65 +289,11 @@ export function AddDocument({
<ConnectContent selectedProject={localSelectedProject} />
)}
<div className="flex justify-between">
<DropdownMenu
open={isProjectSelectorOpen}
onOpenChange={setIsProjectSelectorOpen}
>
<DropdownMenuTrigger asChild>
<Button
variant="insideOut"
className="gap-2"
disabled={isSubmitting}
>
<FolderIcon className="size-4" />
<span className="max-w-[120px] truncate">
{isLoadingProjects ? "..." : projectName}
</span>
<motion.div
animate={{ rotate: isProjectSelectorOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronsUpDownIcon className="size-4" color="#737373" />
</motion.div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-56 bg-[#1B1F24] border border-[#2A2E35] rounded-[12px] p-1.5 max-h-64 overflow-y-auto"
>
<DropdownMenuItem
onClick={() => handleProjectSelect(DEFAULT_PROJECT_ID)}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-[8px] cursor-pointer",
localSelectedProject === DEFAULT_PROJECT_ID
? "bg-[#4BA0FA]/20 text-white"
: "text-[#737373] hover:bg-[#14161A] hover:text-white",
)}
>
<FolderIcon className="h-4 w-4" />
<span className="text-sm font-medium">Default Project</span>
</DropdownMenuItem>
{projects
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
.map((project: Project) => (
<DropdownMenuItem
key={project.id}
onClick={() => handleProjectSelect(project.containerTag)}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-[8px] cursor-pointer",
localSelectedProject === project.containerTag
? "bg-[#4BA0FA]/20 text-white"
: "text-[#737373] hover:bg-[#14161A] hover:text-white",
)}
>
<FolderIcon className="h-4 w-4" />
<span className="text-sm font-medium truncate">
{project.name}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<SpaceSelector
value={localSelectedProject}
onValueChange={setLocalSelectedProject}
variant="insideOut"
/>
<div className="flex items-center gap-2">
<Button
variant="ghost"
Expand Down
24 changes: 22 additions & 2 deletions apps/web/components/new/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,28 @@ export function ChatSidebar({
>({})
const [isInputExpanded, setIsInputExpanded] = useState(false)
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true)
const [heightOffset, setHeightOffset] = useState(95)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const messagesContainerRef = useRef<HTMLDivElement>(null)
const { selectedProject } = useProject()
const { setCurrentChatId } = usePersistentChat()

// Adjust chat height based on scroll position
useEffect(() => {
const handleWindowScroll = () => {
const scrollThreshold = 80
const scrollY = window.scrollY
const progress = Math.min(scrollY / scrollThreshold, 1)
const newOffset = 95 - progress * (95 - 15)
setHeightOffset(newOffset)
}

window.addEventListener("scroll", handleWindowScroll, { passive: true })
handleWindowScroll()

return () => window.removeEventListener("scroll", handleWindowScroll)
}, [])

const { messages, sendMessage, status, setMessages, stop } = useChat({
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
Expand Down Expand Up @@ -364,7 +381,7 @@ export function ChatSidebar({
>
<motion.button
onClick={toggleChat}
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
className="flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap"
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
Expand All @@ -377,9 +394,12 @@ export function ChatSidebar({
<motion.div
key="open"
className={cn(
"w-[450px] h-[calc(100vh-95px)] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
"w-[450px] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
dmSansClassName(),
)}
style={{
height: `calc(100vh - ${heightOffset}px)`,
}}
initial={{ x: "100px", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: "100px", opacity: 0 }}
Expand Down
68 changes: 6 additions & 62 deletions apps/web/components/new/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import { useAuth } from "@lib/auth-context"
import { useEffect, useState } from "react"
import {
ChevronsLeftRight,
LayoutGridIcon,
Plus,
SearchIcon,
FolderIcon,
LogOut,
Settings,
Home,
Expand All @@ -20,22 +18,18 @@ import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { useProjectName } from "@/hooks/use-project-name"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { useQuery } from "@tanstack/react-query"
import { $fetch } from "@repo/lib/api"
import { authClient } from "@lib/auth"
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
import Link from "next/link"
import type { Project } from "@repo/lib/types"
import { SpaceSelector } from "./space-selector"

interface HeaderProps {
onAddMemory?: () => void
Expand All @@ -45,23 +39,9 @@ interface HeaderProps {
export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
const { user } = useAuth()
const [name, setName] = useState<string>("")
const projectName = useProjectName()
const { selectedProject } = useProject()
const { switchProject } = useProjectMutations()
const router = useRouter()
const { data: projects = [] } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
const response = await $fetch("@get/projects")

if (response.error) {
throw new Error(response.error?.message || "Failed to load projects")
}

return response.data?.projects || []
},
staleTime: 30 * 1000,
})

useEffect(() => {
const storedName =
Expand Down Expand Up @@ -130,47 +110,11 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
</DropdownMenuContent>
</DropdownMenu>
<div className="self-stretch w-px bg-[#FFFFFF33]" />
<div className="flex items-center gap-2">
<p>📁 {projectName}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="cursor-pointer hover:opacity-70 transition-opacity"
aria-label="Change project"
>
<ChevronsLeftRight className="size-4 rotate-90" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuItem
onClick={() => switchProject(DEFAULT_PROJECT_ID)}
className={cn(
"cursor-pointer",
selectedProject === DEFAULT_PROJECT_ID && "bg-accent",
)}
>
<FolderIcon className="h-3.5 w-3.5 mr-2" />
<span className="text-sm">Default Project</span>
</DropdownMenuItem>
{projects
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
.map((project: Project) => (
<DropdownMenuItem
key={project.id}
onClick={() => switchProject(project.containerTag)}
className={cn(
"cursor-pointer",
selectedProject === project.containerTag && "bg-accent",
)}
>
<FolderIcon className="h-3.5 w-3.5 mr-2 opacity-70" />
<span className="text-sm truncate">{project.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<SpaceSelector
value={selectedProject}
onValueChange={switchProject}
showChevron
/>
</div>
<Tabs defaultValue="grid">
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
Expand Down
Loading
Loading