mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-14 09:01:45 +00:00
feat: major iteration on the app
This commit is contained in:
parent
e672af6b3d
commit
29fe0f4eaa
14 changed files with 888 additions and 581 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
82
apps/web/components/chat/home-chat-composer.tsx
Normal file
82
apps/web/components/chat/home-chat-composer.tsx
Normal 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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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]" />
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 |
Loading…
Add table
Reference in a new issue