feat: major iteration on the app

This commit is contained in:
Mahesh Sanikommmu 2026-04-15 09:21:06 -07:00
parent e672af6b3d
commit 29fe0f4eaa
14 changed files with 888 additions and 581 deletions

View file

@ -47,7 +47,7 @@ export default function SetupPage() {
}
return (
<div className="h-screen overflow-hidden bg-black">
<div className="relative h-screen overflow-hidden bg-black">
<SetupHeader />
<AnimatedGradientBackground animateFromBottom={false} />

View file

@ -207,7 +207,7 @@ export default function WelcomePage() {
const showUserSupermemory = currentStep === "username"
return (
<div className="h-screen overflow-hidden bg-black">
<div className="relative h-screen overflow-hidden bg-black">
<InitialHeader
showUserSupermemory={
currentStep === "features" || currentStep === "memories"

View file

@ -3,11 +3,12 @@
import { useState, useCallback, useEffect } from "react"
import { useQueryState } from "nuqs"
import { Header } from "@/components/header"
import { ChatSidebar } from "@/components/chat"
import { ChatSidebar, HomeChatComposer } from "@/components/chat"
import { MemoriesGrid } from "@/components/memories-grid"
import { GraphLayoutView } from "@/components/graph-layout-view"
import { IntegrationsView } from "@/components/integrations-view"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
// AnimatedGradientBackground — commented for now (see helpers + usage below).
// import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { AddDocumentModal } from "@/components/add-document"
import { DocumentModal } from "@/components/document-modal"
import { DocumentsCommandPalette } from "@/components/documents-command-palette"
@ -15,7 +16,6 @@ import { FullscreenNoteModal } from "@/components/fullscreen-note-modal"
import type { HighlightItem } from "@/components/highlights-card"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
import { useIsMobile } from "@hooks/use-mobile"
import { useAuth } from "@lib/auth-context"
import { useProject } from "@/stores"
@ -26,6 +26,7 @@ import {
useQuickNoteDraft,
} from "@/stores/quick-note-draft"
import { analytics } from "@/lib/analytics"
import type { ModelId } from "@/lib/models"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
@ -39,7 +40,6 @@ import {
qParam,
docParam,
fullscreenParam,
chatParam,
integrationParam,
pluginsPanelParam,
type IntegrationParamValue,
@ -48,6 +48,30 @@ import {
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
/*
* Animated gradient — commented for now (restore with import + JSX in NewPage).
function subscribeViewportWidth(cb: () => void) {
window.addEventListener("resize", cb)
return () => window.removeEventListener("resize", cb)
}
function getViewportWidth() {
return window.innerWidth
}
const GRADIENT_TOP_WIDTH_MAX = 1440
function gradientTopPositionForWidth(width: number) {
const minW = 320
const pctWide = 15
const pctNarrow = 70
const w = Math.min(GRADIENT_TOP_WIDTH_MAX, Math.max(minW, width))
const t = (w - minW) / (GRADIENT_TOP_WIDTH_MAX - minW)
const eased = t * t
return `${Math.round(pctNarrow + eased * (pctWide - pctNarrow))}%`
}
*/
function ViewErrorFallback() {
return (
<div className="flex-1 flex items-center justify-center p-8">
@ -122,7 +146,6 @@ export default function NewPage() {
"fullscreen",
fullscreenParam,
)
const [isChatOpen, setIsChatOpen] = useQueryState("chat", chatParam)
const [integrationFromUrl, setIntegration] = useQueryState(
"integration",
integrationParam,
@ -138,6 +161,10 @@ export default function NewPage() {
// Ephemeral local state (not worth URL-encoding)
const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
const [queuedChatSeed, setQueuedChatSeed] = useState<string | null>(null)
const [queuedChatModel, setQueuedChatModel] = useState<ModelId | null>(null)
const [queuedMessageSource, setQueuedMessageSource] = useState<
"highlight" | "home"
>("highlight")
const [selectedDocument, setSelectedDocument] =
useState<DocumentWithMemories | null>(null)
@ -375,11 +402,29 @@ export default function NewPage() {
const handleHighlightsChat = useCallback(
(seed: string) => {
setQueuedChatSeed(seed)
setIsChatOpen(true)
setQueuedChatModel(null)
setQueuedMessageSource("highlight")
void setViewMode("chat")
},
[setIsChatOpen],
[setViewMode],
)
const handleHomeChatStart = useCallback(
(message: string, model: ModelId) => {
setQueuedChatSeed(message)
setQueuedChatModel(model)
setQueuedMessageSource("home")
void setViewMode("chat")
},
[setViewMode],
)
const consumeQueuedChat = useCallback(() => {
setQueuedChatSeed(null)
setQueuedChatModel(null)
setQueuedMessageSource("highlight")
}, [])
const handleHighlightsShowRelated = useCallback(
(query: string) => {
analytics.searchOpened({ source: "highlight_related" })
@ -409,25 +454,30 @@ export default function NewPage() {
[setAddDoc],
)
const chatOpen = isChatOpen !== null ? isChatOpen : !isMobile
const isChatView = viewMode === "chat"
const isGraphMode = viewMode === "graph" && !isMobile
// Animated gradient — commented for now (use with useSyncExternalStore + helpers above).
// const viewportWidth = useSyncExternalStore(
// subscribeViewportWidth,
// getViewportWidth,
// () => GRADIENT_TOP_WIDTH_MAX,
// )
// const gradientTopPosition = gradientTopPositionForWidth(viewportWidth)
return (
<HotkeysProvider>
<div
className={cn(
"bg-black min-h-screen",
isGraphMode && "h-screen overflow-hidden",
"relative flex min-h-dvh flex-col bg-black",
isGraphMode && "h-dvh overflow-hidden",
)}
>
<AnimatedGradientBackground
topPosition="15%"
animateFromBottom={false}
/>
{/* AnimatedGradientBackground: off for now (import + helpers commented above). */}
{isGraphMode && (
<div
id="graph-dotted-grid"
className="absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.25)_1px,transparent_1px)] bg-size-[32px_32px] mask-[radial-gradient(ellipse_at_center,black_60%,transparent_100%)]"
className="pointer-events-none absolute inset-0 z-[1] bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.25)_1px,transparent_1px)] bg-size-[32px_32px] mask-[radial-gradient(ellipse_at_center,black_60%,transparent_100%)]"
/>
)}
<Header
@ -435,33 +485,56 @@ export default function NewPage() {
analytics.addDocumentModalOpened()
setAddDoc("note")
}}
onOpenChat={() => setIsChatOpen(true)}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
<main
key={`main-container-${chatOpen}-${viewMode}`}
key={`main-container-${viewMode}`}
className={cn(
"z-10 relative",
isGraphMode && "h-[calc(100vh-86px)] overflow-hidden",
"relative z-10 flex min-h-0 flex-1 flex-col",
(isGraphMode || isChatView) && "overflow-hidden",
)}
>
<div className={cn("relative z-10 flex flex-col md:flex-row h-full")}>
<div
className={cn(
"relative z-10 flex min-h-0 flex-1 flex-col md:flex-row",
)}
>
<ErrorBoundary fallback={<ViewErrorFallback />}>
{viewMode === "integrations" ? (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
{isChatView ? (
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col md:self-stretch">
<ChatSidebar
layout="page"
isChatOpen
setIsChatOpen={(open) => {
if (!open) void setViewMode("list")
}}
queuedMessage={queuedChatSeed}
onConsumeQueuedMessage={consumeQueuedChat}
queuedMessageSource={queuedMessageSource}
initialSelectedModel={queuedChatModel}
emptyStateSuggestions={highlightsData?.questions}
/>
</div>
) : viewMode === "integrations" ? (
<div className="min-h-0 min-w-0 flex-1 p-4 pt-2! md:p-6 md:pr-0">
<IntegrationsView />
</div>
) : viewMode === "graph" && !isMobile ? (
<div className="flex-1">
<GraphLayoutView isChatOpen={chatOpen} />
<div className="min-h-0 min-w-0 flex-1">
<GraphLayoutView />
</div>
) : (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<div
className={cn(
"min-h-0 min-w-0 flex-1 p-4 pt-2! md:p-6 md:pr-0",
viewMode === "list" && "pb-32 md:pb-36",
)}
>
<MemoriesGrid
isChatOpen={chatOpen}
isChatOpen={false}
onOpenDocument={handleOpenDocument}
isSelectionMode={isSelectionMode}
selectedDocumentIds={selectedDocumentIds}
@ -499,30 +572,20 @@ export default function NewPage() {
</div>
)}
</ErrorBoundary>
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
<AnimatePresence mode="popLayout">
<ErrorBoundary>
<ChatSidebar
isChatOpen={chatOpen}
setIsChatOpen={(open) => setIsChatOpen(open)}
queuedMessage={queuedChatSeed}
onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
emptyStateSuggestions={highlightsData?.questions}
/>
</ErrorBoundary>
</AnimatePresence>
</div>
</div>
</main>
{isMobile && (
<ChatSidebar
isChatOpen={chatOpen}
setIsChatOpen={(open) => setIsChatOpen(open)}
queuedMessage={queuedChatSeed}
onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
emptyStateSuggestions={highlightsData?.questions}
/>
{viewMode === "list" && (
<div
className={cn(
"pointer-events-none fixed inset-x-0 z-30",
isMobile ? "bottom-[4.5rem]" : "bottom-0",
)}
>
<div className="pointer-events-auto">
<HomeChatComposer onStartChat={handleHomeChatStart} />
</div>
</div>
)}
<AddDocumentModal

View file

@ -8,7 +8,7 @@ export function AnimatedGradientBackground({
animateFromBottom?: boolean
}) {
return (
<div className="fixed inset-0 z-0 overflow-hidden">
<div className="pointer-events-none absolute inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-0 left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-0.png')] bg-size-[150%_auto] bg-top bg-no-repeat"
style={{ top: animateFromBottom ? undefined : topPosition }}
@ -56,7 +56,7 @@ export function AnimatedGradientBackground({
}}
/>
<motion.div
className="absolute top-0 left-0 right-0 bottom-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat"
className="absolute inset-0 bg-[url('/bg-rectangle.png')] bg-cover bg-bottom bg-no-repeat"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",

View file

@ -0,0 +1,82 @@
"use client"
import { useCallback, useMemo, useState } from "react"
import ChatInput from "./input"
import ChatModelSelector from "./model-selector"
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
import { useProject } from "@/stores"
import { useContainerTags } from "@/hooks/use-container-tags"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import type { ModelId } from "@/lib/models"
export function HomeChatComposer({
onStartChat,
className,
}: {
onStartChat: (message: string, model: ModelId) => void
className?: string
}) {
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
const { selectedProject } = useProject()
const { allProjects } = useContainerTags()
const chatSpaceLabel = useMemo(
() =>
getChatSpaceDisplayLabel({
selectedProject,
allProjects,
}),
[selectedProject, allProjects],
)
const send = useCallback(() => {
const t = input.trim()
if (!t) return
onStartChat(t, selectedModel)
setInput("")
}, [input, onStartChat, selectedModel])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
send()
}
}
return (
<div className={cn("border-[#17181AB2]/80 backdrop-blur-md", className)}>
<div className="mx-auto w-full max-w-[720px] px-4 pt-1 pb-3 md:pb-4">
<ChatInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSend={send}
onStop={() => {}}
onKeyDown={handleKeyDown}
isResponding={false}
activeStatus="Waiting for input..."
stackedToolbar={
<>
<ChatModelSelector
selectedModel={selectedModel}
onModelChange={setSelectedModel}
minimal
/>
<div
className={cn(
"inline-flex max-w-[min(160px,35vw)] min-w-0 shrink items-center rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5",
dmSansClassName(),
)}
title={chatSpaceLabel}
>
<span className="truncate text-sm text-[#FAFAFA]">
{chatSpaceLabel}
</span>
</div>
</>
}
/>
</div>
</div>
)
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import { ChevronUpIcon } from "lucide-react"
import NovaOrb from "@/components/nova/nova-orb"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useRef, useState } from "react"
import { type ReactNode, useRef, useState } from "react"
import { motion } from "motion/react"
import { SendButton, StopButton } from "./actions"
@ -18,6 +18,8 @@ interface ChatInputProps {
activeStatus?: string
chainOfThoughtComponent?: React.ReactNode
onExpandedChange?: (expanded: boolean) => void
/** Model + space controls on one row with send; textarea full-width above */
stackedToolbar?: ReactNode
}
export default function ChatInput({
@ -30,6 +32,7 @@ export default function ChatInput({
activeStatus,
chainOfThoughtComponent,
onExpandedChange,
stackedToolbar,
}: ChatInputProps) {
const [isMultiline, setIsMultiline] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
@ -103,31 +106,59 @@ export default function ChatInput({
/>
)}
</button>
<div
className={cn(
"flex items-end gap-2 bg-[#070E1B] rounded-xl p-2 border-[#52596633] border focus-within:outline-[#525D6EB2] focus-within:outline-1 transition-all duration-200",
isMultiline && "flex-col",
)}
>
<textarea
ref={textareaRef}
value={value}
onChange={handleChange}
onKeyDown={onKeyDown}
placeholder="Ask your supermemory..."
className="bg-transparent w-full p-2 placeholder:text-[#525D6E] focus:outline-none resize-none overflow-y-auto transition-all duration-200"
style={{ minHeight: "36px" }}
rows={1}
disabled={isResponding}
/>
<div className="transition-all duration-200">
{isResponding ? (
<StopButton onClick={onStop} />
) : (
<SendButton onClick={onSend} disabled={!value.trim()} />
)}
{stackedToolbar ? (
<div className="flex flex-col gap-2 rounded-xl border border-[#52596633] bg-[#070E1B] p-2 transition-all duration-200 focus-within:outline-1 focus-within:outline-[#525D6EB2]">
<textarea
ref={textareaRef}
value={value}
onChange={handleChange}
onKeyDown={onKeyDown}
placeholder="Ask your supermemory..."
className="w-full resize-none overflow-y-auto bg-transparent p-2 transition-all duration-200 placeholder:text-[#525D6E] focus:outline-none"
style={{ minHeight: "36px" }}
rows={1}
disabled={isResponding}
/>
<div className="flex items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
{stackedToolbar}
</div>
<div className="shrink-0">
{isResponding ? (
<StopButton onClick={onStop} />
) : (
<SendButton onClick={onSend} disabled={!value.trim()} />
)}
</div>
</div>
</div>
</div>
) : (
<div
className={cn(
"flex items-end gap-2 rounded-xl border border-[#52596633] bg-[#070E1B] p-2 transition-all duration-200 focus-within:outline-1 focus-within:outline-[#525D6EB2]",
isMultiline && "flex-col",
)}
>
<textarea
ref={textareaRef}
value={value}
onChange={handleChange}
onKeyDown={onKeyDown}
placeholder="Ask your supermemory..."
className="w-full resize-none overflow-y-auto bg-transparent p-2 transition-all duration-200 placeholder:text-[#525D6E] focus:outline-none"
style={{ minHeight: "36px" }}
rows={1}
disabled={isResponding}
/>
<div className="transition-all duration-200">
{isResponding ? (
<StopButton onClick={onStop} />
) : (
<SendButton onClick={onSend} disabled={!value.trim()} />
)}
</div>
</div>
)}
</motion.div>
)
}

View file

@ -11,11 +11,14 @@ import { analytics } from "@/lib/analytics"
interface ChatModelSelectorProps {
selectedModel?: ModelId
onModelChange?: (model: ModelId) => void
/** Compact pill matching inline send control (black + #161F2C border, rounded-full) */
minimal?: boolean
}
export default function ChatModelSelector({
selectedModel: selectedModelProp,
onModelChange,
minimal = false,
}: ChatModelSelectorProps = {}) {
const [internalModel, setInternalModel] = useState<ModelId>("gemini-2.5-pro")
const [isOpen, setIsOpen] = useState(false)
@ -33,25 +36,44 @@ export default function ChatModelSelector({
setIsOpen(false)
}
const trigger = minimal ? (
<button
type="button"
className={cn(
"flex max-w-[min(100%,220px)] min-w-0 shrink cursor-pointer items-center gap-1.5 rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5 text-sm transition-colors hover:bg-[#161F2C]",
dmSansClassName(),
)}
onClick={() => setIsOpen(!isOpen)}
>
<p className="min-w-0 truncate text-left text-[#FAFAFA]">
{currentModelData.name}{" "}
<span className="text-[#525D6E]">{currentModelData.version}</span>
</p>
<ChevronDownIcon className="size-3.5 shrink-0 text-[#525D6E]" />
</button>
) : (
<Button
variant="headers"
className={cn(
"h-10! max-w-[min(100%,220px)] shrink gap-1 rounded-full border-[#73737333] bg-[#0D121A] text-base",
dmSansClassName(),
)}
style={{
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
}}
onClick={() => setIsOpen(!isOpen)}
>
<p className="truncate text-sm">
{currentModelData.name}{" "}
<span className="text-[#737373]">{currentModelData.version}</span>
</p>
<ChevronDownIcon className="size-4 text-[#737373]" />
</Button>
)
return (
<div className="relative flex items-center gap-2">
<Button
variant="headers"
className={cn(
"rounded-full text-base gap-1 h-10! border-[#73737333] bg-[#0D121A]",
dmSansClassName(),
)}
style={{
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
}}
onClick={() => setIsOpen(!isOpen)}
>
<p className="text-sm">
{currentModelData.name}{" "}
<span className="text-[#737373]">{currentModelData.version}</span>
</p>
<ChevronDownIcon className="size-4 text-[#737373]" />
</Button>
<div className="relative flex min-w-0 shrink items-center gap-2">
{trigger}
{isOpen && (
<>

View file

@ -12,11 +12,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { ShareModal } from "./share-modal"
import { shareParam } from "@/lib/search-params"
interface GraphLayoutViewProps {
isChatOpen: boolean
}
export const GraphLayoutView = memo<GraphLayoutViewProps>(({ isChatOpen }) => {
export const GraphLayoutView = memo(function GraphLayoutView() {
const { effectiveContainerTags } = useProject()
const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
const [isShareModalOpen, setIsShareModalOpen] = useQueryState(
@ -34,14 +30,14 @@ export const GraphLayoutView = memo<GraphLayoutViewProps>(({ isChatOpen }) => {
}, [setIsShareModalOpen])
return (
<div className="relative w-full h-[calc(100vh-86px)]">
<div className="relative h-full min-h-0 w-full">
{/* Full-width graph */}
<div className="absolute inset-0">
<MemoryGraph
containerTags={effectiveContainerTags}
variant="consumer"
highlightDocumentIds={allHighlightDocumentIds}
highlightsVisible={isChatOpen}
highlightsVisible
maxNodes={undefined}
canvasRef={canvasRef}
/>

View file

@ -3,7 +3,6 @@
import { Logo } from "@ui/assets/Logo"
import { useAuth } from "@lib/auth-context"
import {
LayoutGridIcon,
Plus,
SearchIcon,
Settings,
@ -17,7 +16,6 @@ import {
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 { GraphIcon, IntegrationsIcon } from "@/components/integration-icons"
import {
DropdownMenu,
@ -34,17 +32,16 @@ import { useIsMobile } from "@hooks/use-mobile"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { UserProfileMenu } from "@/components/user-profile-menu"
import { FeedbackModal } from "./feedback-modal"
import { useViewMode, type ViewMode } from "@/lib/view-mode-context"
import { useViewMode } from "@/lib/view-mode-context"
import { useQueryState } from "nuqs"
import { feedbackParam } from "@/lib/search-params"
interface HeaderProps {
onAddMemory?: () => void
onOpenChat?: () => void
onOpenSearch?: () => void
}
export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
const { user, isRestoring } = useAuth()
const { selectedProjects, setSelectedProjects } = useProject()
const router = useRouter()
@ -65,7 +62,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
return (
<div className="flex p-3 md:p-4 justify-between items-center gap-2">
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-3 md:p-4">
<div className="flex items-center justify-center gap-2 md:gap-4 z-10! min-w-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -140,45 +137,62 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
)}
</div>
{!isMobile && (
<Tabs
value={viewMode === "list" ? "grid" : viewMode}
onValueChange={(v) =>
setViewMode(v === "grid" ? "list" : (v as ViewMode))
}
>
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
<TabsTrigger
value="grid"
className={cn(
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<LayoutGridIcon className="size-4" />
Grid
</TabsTrigger>
<TabsTrigger
value="graph"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<GraphIcon className="size-4" />
Graph
</TabsTrigger>
<TabsTrigger
value="integrations"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<IntegrationsIcon className="size-4" />
Integrations
</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-2 z-10!">
<button
type="button"
onClick={() => void setViewMode("list")}
aria-label="Home"
aria-current={viewMode === "list" ? "page" : undefined}
className={cn(
"flex size-11 shrink-0 items-center justify-center rounded-full border cursor-pointer transition-colors",
viewMode === "list"
? "border-[#2261CA33] bg-[#00173C] text-white"
: "border-[#161F2C] bg-muted text-muted-foreground hover:bg-white/5",
dmSansClassName(),
)}
>
<Home className="size-4" />
</button>
<div
role="tablist"
aria-orientation="horizontal"
className="bg-muted text-muted-foreground inline-flex h-11 w-fit items-center justify-center gap-0.5 rounded-full border border-[#161F2C] p-1 z-10!"
>
{(
[
{ mode: "graph" as const, label: "Graph", icon: GraphIcon },
{
mode: "integrations" as const,
label: "Integrations",
icon: IntegrationsIcon,
},
{
mode: "chat" as const,
label: "Chat",
icon: MessageCircleIcon,
},
] as const
).map(({ mode, label, icon: Icon }) => (
<button
key={mode}
type="button"
role="tab"
aria-selected={viewMode === mode}
onClick={() => void setViewMode(mode)}
className={cn(
"inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-full border border-transparent px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors cursor-pointer",
viewMode === mode
? "border-[#2261CA33] bg-[#00173C] text-white shadow-md"
: "text-foreground hover:bg-white/5",
dmSansClassName(),
)}
>
<Icon className="size-4" />
{label}
</button>
))}
</div>
</div>
)}
<div className="flex items-center gap-2 z-10!">
{isMobile ? (
@ -225,7 +239,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
Integrations
</DropdownMenuItem>
<DropdownMenuItem
onClick={onOpenChat}
onClick={() => void setViewMode("chat")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<MessageCircleIcon className="h-4 w-4 text-[#737373]" />

View file

@ -29,7 +29,7 @@ export const analytics = {
chatHistoryViewed: () => safeCapture("chat_history_viewed"),
chatDeleted: () => safeCapture("chat_deleted"),
viewModeChanged: (mode: "graph" | "list" | "integrations") =>
viewModeChanged: (mode: "graph" | "list" | "integrations" | "chat") =>
safeCapture("view_mode_changed", { mode }),
documentCardClicked: () => safeCapture("document_card_clicked"),
@ -110,8 +110,9 @@ export const analytics = {
}) => safeCapture("highlight_clicked", props),
// chat analytics
chatMessageSent: (props: { source: "typed" | "suggested" | "highlight" }) =>
safeCapture("chat_message_sent", props),
chatMessageSent: (props: {
source: "typed" | "suggested" | "highlight" | "home"
}) => safeCapture("chat_message_sent", props),
chatSuggestedQuestionClicked: () =>
safeCapture("chat_suggested_question_clicked"),

View file

@ -23,7 +23,7 @@ export const shareParam = parseAsBoolean.withDefault(false)
export const feedbackParam = parseAsBoolean.withDefault(false)
// View & filter states
const viewLiterals = ["graph", "list", "integrations"] as const
const viewLiterals = ["graph", "list", "integrations", "chat"] as const
const integrationLiterals = ["import", "chrome", "connections"] as const
export type IntegrationParamValue = (typeof integrationLiterals)[number]
export const integrationParam = parseAsStringLiteral(integrationLiterals)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 MiB