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
42 changes: 40 additions & 2 deletions apps/web/app/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
"use client"

import { useState } from "react"
import { useState, useCallback } from "react"
import { Header } from "@/components/new/header"
import { ChatSidebar } from "@/components/new/chat"
import { MemoriesGrid } from "@/components/new/memories-grid"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
import { AddDocumentModal } from "@/components/new/add-document"
import { MCPModal } from "@/components/new/mcp-modal"
import { DocumentModal } from "@/components/new/document-modal"
import { DocumentsCommandPalette } from "@/components/new/documents-command-palette"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
import { useIsMobile } from "@hooks/use-mobile"
import { useProject } from "@/stores"
import { analytics } from "@/lib/analytics"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"

type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

export default function NewPage() {
const isMobile = useIsMobile()
const { selectedProject } = useProject()
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
const [isMCPModalOpen, setIsMCPModalOpen] = useState(false)
const [isSearchOpen, setIsSearchOpen] = useState(false)
const [selectedDocument, setSelectedDocument] =
useState<DocumentWithMemories | null>(null)
const [isDocumentModalOpen, setIsDocumentModalOpen] = useState(false)

useHotkeys("c", () => {
analytics.addDocumentModalOpened()
setIsAddDocumentOpen(true)
})
useHotkeys("mod+k", (e) => {
e.preventDefault()
setIsSearchOpen(true)
})
const [isChatOpen, setIsChatOpen] = useState(!isMobile)

const handleOpenDocument = useCallback((document: DocumentWithMemories) => {
setSelectedDocument(document)
setIsDocumentModalOpen(true)
}, [])

return (
<HotkeysProvider>
<div className="bg-black min-h-screen">
Expand All @@ -40,13 +63,17 @@ export default function NewPage() {
setIsMCPModalOpen(true)
}}
onOpenChat={() => setIsChatOpen(true)}
onOpenSearch={() => setIsSearchOpen(true)}
/>
<main
key={`main-container-${isChatOpen}`}
className="z-10 flex flex-col md:flex-row relative"
>
<div className="flex-1 p-4 md:p-6 md:pr-0">
<MemoriesGrid isChatOpen={isChatOpen} />
<MemoriesGrid
isChatOpen={isChatOpen}
onOpenDocument={handleOpenDocument}
/>
</div>
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
<AnimatePresence mode="popLayout">
Expand All @@ -70,6 +97,17 @@ export default function NewPage() {
isOpen={isMCPModalOpen}
onClose={() => setIsMCPModalOpen(false)}
/>
<DocumentsCommandPalette
open={isSearchOpen}
onOpenChange={setIsSearchOpen}
projectId={selectedProject}
onOpenDocument={handleOpenDocument}
/>
<DocumentModal
document={selectedDocument}
isOpen={isDocumentModalOpen}
onClose={() => setIsDocumentModalOpen(false)}
/>
</div>
</HotkeysProvider>
)
Expand Down
18 changes: 12 additions & 6 deletions apps/web/components/new/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export function ChatSidebar({
className={cn(
"flex items-start justify-start",
isMobile
? "fixed bottom-4 right-4 z-50"
? "fixed bottom-5 right-0 left-0 z-50 justify-center items-center"
: "absolute top-0 right-0 m-4",
dmSansClassName(),
)}
Expand All @@ -392,15 +392,21 @@ export function ChatSidebar({
<motion.button
onClick={toggleChat}
className={cn(
"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 shadow-lg",
isMobile && "px-4 py-2",
"flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border text-white cursor-pointer whitespace-nowrap",
isMobile
? "gap-2.5 px-5 py-3 text-[15px] border-[#1E2128] shadow-[0_8px_32px_rgba(0,0,0,0.5),0_2px_8px_rgba(0,0,0,0.3)]"
: "border-[#17181A] shadow-lg",
)}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
background: isMobile
? "linear-gradient(135deg, #12161C 0%, #0A0D12 100%)"
: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<NovaOrb size={24} className="blur-[0.6px]! z-10" />
{!isMobile && "Chat with Nova"}
<NovaOrb size={isMobile ? 26 : 24} className="blur-[0.6px]! z-10" />
<span className={cn(isMobile && "font-medium")}>Chat with Nova</span>
</motion.button>
</motion.div>
) : (
Expand Down
102 changes: 10 additions & 92 deletions apps/web/components/new/document-cards/file-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,12 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { PDF } from "@ui/assets/icons"
import { FileText, Image, Video } from "lucide-react"
import { DocumentIcon } from "@/components/new/document-icon"

type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

function PDFIcon() {
return (
<svg
width="8"
height="10"
viewBox="0 0 8 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>PDF Icon</title>
<g filter="url(#filter0_i_719_6586)">
<path
d="M1 10C0.725 10 0.489583 9.90208 0.29375 9.70625C0.0979167 9.51042 0 9.275 0 9V1C0 0.725 0.0979167 0.489583 0.29375 0.29375C0.489583 0.0979167 0.725 0 1 0H5L8 3V9C8 9.275 7.90208 9.51042 7.70625 9.70625C7.51042 9.90208 7.275 10 7 10H1ZM4.5 3.5V1H1V9H7V3.5H4.5Z"
fill="#FF7673"
/>
</g>
<defs>
<filter
id="filter0_i_719_6586"
x="0"
y="0"
width="8.25216"
height="10.2522"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="0.252163" dy="0.252163" />
<feGaussianBlur stdDeviation="0.504325" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0431373 0 0 0 0 0.0588235 0 0 0 0 0.0823529 0 0 0 0.4 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_719_6586"
/>
</filter>
</defs>
</svg>
)
}

function getFileTypeInfo(document: DocumentWithMemories): {
icon: React.ReactNode
extension: string
color?: string
} {
Expand All @@ -78,56 +19,33 @@ function getFileTypeInfo(document: DocumentWithMemories): {

if (mimeType) {
if (mimeType === "application/pdf") {
return {
icon: <PDFIcon />,
extension: ".pdf",
color: "#FF7673",
}
return { extension: ".pdf", color: "#FF7673" }
}
if (mimeType.startsWith("image/")) {
const ext = mimeType.split("/")[1] || "jpg"
return {
icon: <Image className="w-4 h-4" style={{ color: "#FAFAFA" }} />,
extension: `.${ext}`,
}
return { extension: `.${ext}` }
}
if (mimeType.startsWith("video/")) {
const ext = mimeType.split("/")[1] || "mp4"
return {
icon: <Video className="w-4 h-4" style={{ color: "#FAFAFA" }} />,
extension: `.${ext}`,
}
return { extension: `.${ext}` }
}
}

switch (type) {
case "pdf":
return {
icon: <PDF className="w-4 h-4 text-[#FF7673]" />,
extension: ".pdf",
color: "#FF7673",
}
return { extension: ".pdf", color: "#FF7673" }
case "image":
return {
icon: <Image className="w-4 h-4" style={{ color: "#FAFAFA" }} />,
extension: ".jpg",
}
return { extension: ".jpg" }
case "video":
return {
icon: <Video className="w-4 h-4" style={{ color: "#FAFAFA" }} />,
extension: ".mp4",
}
return { extension: ".mp4" }
default:
return {
icon: <FileText className="w-4 h-4" style={{ color: "#FAFAFA" }} />,
extension: ".file",
}
return { extension: ".file" }
}
}

export function FilePreview({ document }: { document: DocumentWithMemories }) {
const [imageError, setImageError] = useState(false)
const { icon, extension, color } = getFileTypeInfo(document)
const { extension, color } = getFileTypeInfo(document)

const type = document.type?.toLowerCase()
const mimeType = document.metadata?.mimeType as string | undefined
Expand Down Expand Up @@ -168,7 +86,7 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) {
) : (
<div className="p-3">
<div className="flex items-center gap-1 mb-2">
{icon}
<DocumentIcon type={document.type} url={document.url} className="w-4 h-4" />
<p
className={cn(dmSansClassName(), "text-[10px] font-semibold")}
style={{ color: color }}
Expand Down
51 changes: 17 additions & 34 deletions apps/web/components/new/document-cards/google-docs-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import {
DocumentIcon,
getDocumentTypeLabel,
} from "@/components/new/document-icon"

type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
Expand All @@ -13,49 +17,28 @@ export function GoogleDocsPreview({
}: {
document: DocumentWithMemories
}) {
const label = getDocumentTypeLabel(document.type)

return (
<div className="bg-[#0B1017] p-3 rounded-[18px] gap-3">
<div className="flex items-center gap-2 mb-2">
<svg
className="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 87.3 78"
aria-label="Google Docs"
>
<title>Google Docs</title>
<path
fill="#0066da"
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3L27.5 53H0c0 1.55.4 3.1 1.2 4.5z"
/>
<path
fill="#00ac47"
d="M43.65 25 29.9 1.2c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44A9.06 9.06 0 0 0 0 53h27.5z"
/>
<path
fill="#ea4335"
d="M73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75L86.1 57.5c.8-1.4 1.2-2.95 1.2-4.5H59.798l5.852 11.5z"
/>
<path
fill="#00832d"
d="M43.65 25 57.4 1.2C56.05.4 54.5 0 52.9 0H34.4c-1.6 0-3.15.45-4.5 1.2z"
/>
<path
fill="#2684fc"
d="M59.8 53H27.5L13.75 76.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
/>
<path
fill="#ffba00"
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3L43.65 25 59.8 53h27.45c0-1.55-.4-3.1-1.2-4.5z"
/>
</svg>
<DocumentIcon type={document.type} url={document.url} className="w-4 h-4" />
<p className={cn(dmSansClassName(), "text-[12px] font-semibold")}>
Google Docs
{label}
</p>
</div>
{document.content && (
{document.summary ? (
<p className="text-[10px] text-[#737373] line-clamp-4">
{document.summary}
</p>
) : document.content ? (
<p className="text-[10px] text-[#737373] line-clamp-4">
{document.content}
</p>
) : (
<p className="text-[10px] text-[#737373] line-clamp-4">
No summary available
</p>
)}
</div>
)
Expand Down
Loading
Loading