mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-14 09:01:45 +00:00
add: dashboard improvements
This commit is contained in:
parent
29fe0f4eaa
commit
dc80c2fdbc
12 changed files with 725 additions and 131 deletions
|
|
@ -1,9 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useCallback, useEffect } from "react"
|
||||
import { useState, useCallback, useEffect, useMemo } from "react"
|
||||
import { useQueryState } from "nuqs"
|
||||
import { Header } from "@/components/header"
|
||||
import { ChatSidebar, HomeChatComposer } from "@/components/chat"
|
||||
import { DashboardView } from "@/components/dashboard-view"
|
||||
import { MemoriesGrid } from "@/components/memories-grid"
|
||||
import { GraphLayoutView } from "@/components/graph-layout-view"
|
||||
import { IntegrationsView } from "@/components/integrations-view"
|
||||
|
|
@ -44,6 +45,7 @@ import {
|
|||
pluginsPanelParam,
|
||||
type IntegrationParamValue,
|
||||
} from "@/lib/search-params"
|
||||
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
|
@ -105,6 +107,14 @@ export default function NewPage() {
|
|||
(selectedProjectTag !== undefined &&
|
||||
novaContainerTags.includes(selectedProjectTag))
|
||||
const { allProjects } = useContainerTags()
|
||||
const dashboardSpaceLabel = useMemo(
|
||||
() =>
|
||||
getChatSpaceDisplayLabel({
|
||||
selectedProject,
|
||||
allProjects,
|
||||
}),
|
||||
[selectedProject, allProjects],
|
||||
)
|
||||
const emptyStateSpaceName =
|
||||
!isNovaSpaces && selectedProjectTag
|
||||
? selectedProjectTag === DEFAULT_PROJECT_ID
|
||||
|
|
@ -150,7 +160,10 @@ export default function NewPage() {
|
|||
"integration",
|
||||
integrationParam,
|
||||
)
|
||||
const [pluginsPanelFromUrl] = useQueryState("plugins", pluginsPanelParam)
|
||||
const [pluginsPanelFromUrl, setPluginsPanel] = useQueryState(
|
||||
"plugins",
|
||||
pluginsPanelParam,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationFromUrl || pluginsPanelFromUrl === true) {
|
||||
|
|
@ -446,6 +459,11 @@ export default function NewPage() {
|
|||
[setViewMode, setIntegration],
|
||||
)
|
||||
|
||||
const handleOpenPlugins = useCallback(() => {
|
||||
void setViewMode("integrations")
|
||||
void setPluginsPanel(true)
|
||||
}, [setViewMode, setPluginsPanel])
|
||||
|
||||
const handleAddMemory = useCallback(
|
||||
(tab: "note" | "link") => {
|
||||
analytics.addDocumentModalOpened()
|
||||
|
|
@ -456,6 +474,8 @@ export default function NewPage() {
|
|||
|
||||
const isChatView = viewMode === "chat"
|
||||
const isGraphMode = viewMode === "graph" && !isMobile
|
||||
const isDashboardShell =
|
||||
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
|
||||
|
||||
// Animated gradient — commented for now (use with useSyncExternalStore + helpers above).
|
||||
// const viewportWidth = useSyncExternalStore(
|
||||
|
|
@ -509,7 +529,7 @@ export default function NewPage() {
|
|||
layout="page"
|
||||
isChatOpen
|
||||
setIsChatOpen={(open) => {
|
||||
if (!open) void setViewMode("list")
|
||||
if (!open) void setViewMode("dashboard")
|
||||
}}
|
||||
queuedMessage={queuedChatSeed}
|
||||
onConsumeQueuedMessage={consumeQueuedChat}
|
||||
|
|
@ -526,11 +546,11 @@ export default function NewPage() {
|
|||
<div className="min-h-0 min-w-0 flex-1">
|
||||
<GraphLayoutView />
|
||||
</div>
|
||||
) : (
|
||||
) : viewMode === "list" ? (
|
||||
<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",
|
||||
"pb-10 md:pb-12",
|
||||
)}
|
||||
>
|
||||
<MemoriesGrid
|
||||
|
|
@ -570,12 +590,43 @@ export default function NewPage() {
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DashboardView
|
||||
spaceLabel={dashboardSpaceLabel}
|
||||
headerNotice={
|
||||
viewMode === "graph" && isMobile ? (
|
||||
<div
|
||||
id="graph-mobile-notice"
|
||||
className="rounded-lg border border-[#2261CA33] bg-[#041127] px-3 py-2.5 text-sm text-[#8B8B8B]"
|
||||
>
|
||||
<span className="font-medium text-white">
|
||||
Graph view is available on desktop.
|
||||
</span>{" "}
|
||||
Use a larger screen for the full graph, or keep working
|
||||
from this home view.
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
highlights={highlightsData?.highlights ?? []}
|
||||
isLoadingHighlights={isLoadingHighlights}
|
||||
onAddMemory={handleAddMemory}
|
||||
onOpenSearch={() => {
|
||||
analytics.searchOpened({ source: "header" })
|
||||
setIsSearchOpen(true)
|
||||
}}
|
||||
onOpenIntegrations={handleOpenIntegrations}
|
||||
onOpenPlugins={handleOpenPlugins}
|
||||
onNavigateToMemories={() => void setViewMode("list")}
|
||||
onOpenDocument={handleOpenDocument}
|
||||
onHighlightsChat={handleHighlightsChat}
|
||||
onHighlightsShowRelated={handleHighlightsShowRelated}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{viewMode === "list" && (
|
||||
{isDashboardShell && (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none fixed inset-x-0 z-30",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function HomeChatComposer({
|
|||
onStop={() => {}}
|
||||
onKeyDown={handleKeyDown}
|
||||
isResponding={false}
|
||||
activeStatus="Waiting for input..."
|
||||
showStatusStrip={false}
|
||||
stackedToolbar={
|
||||
<>
|
||||
<ChatModelSelector
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ interface ChatInputProps {
|
|||
onExpandedChange?: (expanded: boolean) => void
|
||||
/** Model + space controls on one row with send; textarea full-width above */
|
||||
stackedToolbar?: ReactNode
|
||||
/** Nova status row + chain-of-thought toggle (off for e.g. home composer) */
|
||||
showStatusStrip?: boolean
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
|
|
@ -33,6 +35,7 @@ export default function ChatInput({
|
|||
chainOfThoughtComponent,
|
||||
onExpandedChange,
|
||||
stackedToolbar,
|
||||
showStatusStrip = true,
|
||||
}: ChatInputProps) {
|
||||
const [isMultiline, setIsMultiline] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
|
@ -55,57 +58,69 @@ export default function ChatInput({
|
|||
<motion.div
|
||||
className={cn("relative z-20!")}
|
||||
animate={{
|
||||
padding: isExpanded ? "16px" : "0",
|
||||
margin: isExpanded ? "0" : "16px",
|
||||
borderRadius: isExpanded ? "0 0 12px 12px" : "12px",
|
||||
backgroundColor: isExpanded ? "#000B1B" : "#01173C",
|
||||
padding: showStatusStrip ? (isExpanded ? "16px" : "0") : "0",
|
||||
margin: showStatusStrip ? (isExpanded ? "0" : "16px") : "0",
|
||||
borderRadius: showStatusStrip
|
||||
? isExpanded
|
||||
? "0 0 12px 12px"
|
||||
: "12px"
|
||||
: "0",
|
||||
backgroundColor: showStatusStrip
|
||||
? isExpanded
|
||||
? "#000B1B"
|
||||
: "#01173C"
|
||||
: "transparent",
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-full left-0 right-0 overflow-hidden transition-all duration-300 ease-out bg-[#000B1B]",
|
||||
isExpanded
|
||||
? "max-h-[60vh] opacity-100 overflow-y-auto pt-1.5 pb-2 rounded-t-xl px-4"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
style={{
|
||||
zIndex: isExpanded ? 50 : 0,
|
||||
}}
|
||||
>
|
||||
{chainOfThoughtComponent}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full p-3 pr-4 flex items-center justify-between cursor-pointer bg-transparent border-0 text-left",
|
||||
!chainOfThoughtComponent && "disabled:cursor-not-allowed",
|
||||
)}
|
||||
onClick={() => {
|
||||
const newExpanded = !isExpanded
|
||||
setIsExpanded(newExpanded)
|
||||
onExpandedChange?.(newExpanded)
|
||||
}}
|
||||
disabled={!chainOfThoughtComponent}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<NovaOrb size={24} className="blur-[1px]! z-10" />
|
||||
<p className={cn("text-[#525D6E]", dmSansClassName())}>
|
||||
{activeStatus || "Waiting for input..."}
|
||||
</p>
|
||||
</div>
|
||||
{chainOfThoughtComponent && (
|
||||
<ChevronUpIcon
|
||||
{showStatusStrip ? (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"size-4 text-[#525D6E] transition-transform duration-300",
|
||||
isExpanded && "rotate-180",
|
||||
"absolute bottom-full left-0 right-0 overflow-hidden transition-all duration-300 ease-out bg-[#000B1B]",
|
||||
isExpanded
|
||||
? "max-h-[60vh] opacity-100 overflow-y-auto pt-1.5 pb-2 rounded-t-xl px-4"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
style={{
|
||||
zIndex: isExpanded ? 50 : 0,
|
||||
}}
|
||||
>
|
||||
{chainOfThoughtComponent}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full p-3 pr-4 flex items-center justify-between cursor-pointer bg-transparent border-0 text-left",
|
||||
!chainOfThoughtComponent && "disabled:cursor-not-allowed",
|
||||
)}
|
||||
onClick={() => {
|
||||
const newExpanded = !isExpanded
|
||||
setIsExpanded(newExpanded)
|
||||
onExpandedChange?.(newExpanded)
|
||||
}}
|
||||
disabled={!chainOfThoughtComponent}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<NovaOrb size={24} className="blur-[1px]! z-10" />
|
||||
<p className={cn("text-[#525D6E]", dmSansClassName())}>
|
||||
{activeStatus || "Waiting for input..."}
|
||||
</p>
|
||||
</div>
|
||||
{chainOfThoughtComponent && (
|
||||
<ChevronUpIcon
|
||||
className={cn(
|
||||
"size-4 text-[#525D6E] transition-transform duration-300",
|
||||
isExpanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{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
|
||||
|
|
|
|||
334
apps/web/components/dashboard-view.tsx
Normal file
334
apps/web/components/dashboard-view.tsx
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { $fetch } from "@lib/api"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
ArrowRight,
|
||||
FileText,
|
||||
LayoutGrid,
|
||||
Link2,
|
||||
SearchIcon,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import type { z } from "zod"
|
||||
import { CHROME_EXTENSION_URL } from "@repo/lib/constants"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Progress } from "@ui/components/progress"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { useProject } from "@/stores"
|
||||
import { useTokenUsage } from "@/hooks/use-token-usage"
|
||||
import { formatUsageNumber, tokensToCredits } from "@/lib/billing-utils"
|
||||
import {
|
||||
HighlightsCard,
|
||||
type HighlightItem,
|
||||
} from "@/components/highlights-card"
|
||||
import { ChromeIcon } from "@/components/integration-icons"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import type { IntegrationParamValue } from "@/lib/search-params"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
const primaryActionClass = cn(
|
||||
"rounded-xl border border-[rgba(82,89,102,0.22)] bg-[#14161A] p-3.5 sm:p-4",
|
||||
"hover:border-[#3374FF]/45 hover:bg-[#1B1F24]",
|
||||
"cursor-pointer text-left transition-colors",
|
||||
)
|
||||
|
||||
const linkChipClass = cn(
|
||||
"rounded-lg border border-[rgba(82,89,102,0.22)] bg-[#101318] px-3 py-2.5",
|
||||
"text-xs font-medium text-white",
|
||||
"hover:border-[#3374FF]/40 hover:bg-[#161A20]",
|
||||
"inline-flex items-center justify-center gap-2 cursor-pointer transition-colors",
|
||||
)
|
||||
|
||||
export function DashboardView({
|
||||
spaceLabel,
|
||||
headerNotice,
|
||||
highlights,
|
||||
isLoadingHighlights,
|
||||
onAddMemory,
|
||||
onOpenSearch,
|
||||
onOpenIntegrations,
|
||||
onOpenPlugins,
|
||||
onNavigateToMemories,
|
||||
onOpenDocument,
|
||||
onHighlightsChat,
|
||||
onHighlightsShowRelated,
|
||||
}: {
|
||||
spaceLabel: string
|
||||
headerNotice?: ReactNode
|
||||
highlights: HighlightItem[]
|
||||
isLoadingHighlights: boolean
|
||||
onAddMemory: (tab: "note" | "link") => void
|
||||
onOpenSearch: () => void
|
||||
onOpenIntegrations: (integration?: IntegrationParamValue) => void
|
||||
onOpenPlugins: () => void
|
||||
onNavigateToMemories: () => void
|
||||
onOpenDocument: (document: DocumentWithMemories) => void
|
||||
onHighlightsChat: (seed: string) => void
|
||||
onHighlightsShowRelated: (query: string) => void
|
||||
}) {
|
||||
const { user } = useAuth()
|
||||
const { effectiveContainerTags } = useProject()
|
||||
const router = useRouter()
|
||||
const autumn = useCustomer()
|
||||
const {
|
||||
tokensUsed,
|
||||
tokensLimit,
|
||||
tokensPercent,
|
||||
searchesUsed,
|
||||
searchesLimit,
|
||||
searchesPercent,
|
||||
currentPlan,
|
||||
hasPaidPlan,
|
||||
isLoading: isUsageLoading,
|
||||
} = useTokenUsage(autumn)
|
||||
|
||||
const { data: recentsData, isPending: isRecentsPending } = useQuery({
|
||||
queryKey: ["dashboard-recents", effectiveContainerTags],
|
||||
queryFn: async (): Promise<DocumentsResponse> => {
|
||||
const response = await $fetch("@post/documents/documents", {
|
||||
body: {
|
||||
page: 1,
|
||||
limit: 8,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
containerTags: effectiveContainerTags,
|
||||
},
|
||||
disableValidation: true,
|
||||
})
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to fetch documents")
|
||||
}
|
||||
return response.data as DocumentsResponse
|
||||
},
|
||||
staleTime: 60 * 1000,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const recents = recentsData?.documents ?? []
|
||||
|
||||
const handleInstallChrome = () => {
|
||||
window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto p-4 pt-2! pb-32 md:p-6 md:pb-36 md:pr-0",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-4xl space-y-6 md:space-y-7">
|
||||
{headerNotice ? <div className="space-y-2">{headerNotice}</div> : null}
|
||||
<header className="space-y-1 border-b border-[#161F2C] pb-5">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-[#525D6E]">
|
||||
Home
|
||||
</p>
|
||||
<h1 className="text-xl font-medium tracking-tight text-white md:text-2xl">
|
||||
{spaceLabel}
|
||||
</h1>
|
||||
<p className="max-w-xl text-sm leading-snug text-[#8B8B8B]">
|
||||
Add memories, search, and open integrations — everything for this
|
||||
space in one place.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wide text-[#525D6E]">
|
||||
Quick actions
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddMemory("link")}
|
||||
className={cn(primaryActionClass, "flex flex-col gap-1.5")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="size-4 shrink-0 text-[#737373] sm:size-5" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Save a link
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-[#4BA0FA]">
|
||||
Add now
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddMemory("note")}
|
||||
className={cn(primaryActionClass, "flex flex-col gap-1.5")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="size-4 shrink-0 text-[#737373] sm:size-5" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Write a note
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-[#4BA0FA]">
|
||||
Add now
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
analytics.searchOpened({ source: "header" })
|
||||
onOpenSearch()
|
||||
}}
|
||||
className={cn(primaryActionClass, "flex flex-col gap-1.5")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchIcon className="size-4 shrink-0 text-[#737373] sm:size-5" />
|
||||
<span className="text-sm font-medium text-white">Search</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-[#4BA0FA]">⌘K</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateToMemories}
|
||||
className={cn(primaryActionClass, "flex flex-col gap-1.5")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="size-4 shrink-0 text-[#737373] sm:size-5" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
All memories
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-[#4BA0FA]">
|
||||
Browse grid
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInstallChrome}
|
||||
className={linkChipClass}
|
||||
>
|
||||
<ChromeIcon className="size-4 shrink-0 text-[#737373]" />
|
||||
<span className="truncate">Chrome</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenIntegrations()}
|
||||
className={linkChipClass}
|
||||
>
|
||||
<Zap className="size-3.5 shrink-0 text-[#737373]" />
|
||||
<span className="truncate">Integrations</span>
|
||||
</button>
|
||||
<button type="button" onClick={onOpenPlugins} className={linkChipClass}>
|
||||
<span className="truncate">Plugins & MCP</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{highlights.length > 0 || isLoadingHighlights ? (
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wide text-[#525D6E]">
|
||||
Highlights
|
||||
</h2>
|
||||
<HighlightsCard
|
||||
items={highlights}
|
||||
onChat={onHighlightsChat}
|
||||
onShowRelated={onHighlightsShowRelated}
|
||||
isLoading={isLoadingHighlights}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wide text-[#525D6E]">
|
||||
Recently saved
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateToMemories}
|
||||
className="text-xs font-medium text-[#4BA0FA] hover:text-[#6BB0FF]"
|
||||
>
|
||||
See all
|
||||
</button>
|
||||
</div>
|
||||
{isRecentsPending ? (
|
||||
<p className="text-sm text-[#525D6E]">Loading…</p>
|
||||
) : recents.length === 0 ? (
|
||||
<p className="text-sm text-[#525D6E]">
|
||||
No memories yet — use quick actions above.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-[#161F2C] rounded-xl border border-[#161F2C] bg-[#080B0F]">
|
||||
{recents.map((doc) => (
|
||||
<li key={doc.id ?? doc.customId}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenDocument(doc)}
|
||||
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-[#0D121A]"
|
||||
>
|
||||
<span className="min-w-0 truncate text-sm text-white">
|
||||
{doc.title?.trim() || "Untitled"}
|
||||
</span>
|
||||
<ArrowRight className="size-4 shrink-0 text-[#525D6E]" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{!isUsageLoading && (
|
||||
<section
|
||||
id="dashboard-usage"
|
||||
className="flex flex-col gap-3 rounded-xl border border-[#161F2C] bg-[#080B0F]/80 px-4 py-3 sm:flex-row sm:items-center sm:gap-5"
|
||||
>
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2 sm:flex-col sm:items-start sm:justify-center sm:gap-0.5">
|
||||
<h2 className="text-xs font-medium text-white">Usage</h2>
|
||||
<span className="text-[11px] capitalize leading-none text-[#8B8B8B]">
|
||||
{currentPlan}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-3 sm:flex-row sm:gap-5">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="flex justify-between text-[10px] text-[#8B8B8B]">
|
||||
<span>Credits</span>
|
||||
<span>
|
||||
{formatUsageNumber(tokensToCredits(tokensUsed))} /{" "}
|
||||
{formatUsageNumber(tokensToCredits(tokensLimit))}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={tokensPercent} className="h-1" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="flex justify-between text-[10px] text-[#8B8B8B]">
|
||||
<span>Searches</span>
|
||||
<span>
|
||||
{formatUsageNumber(searchesUsed)} /{" "}
|
||||
{formatUsageNumber(searchesLimit)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={searchesPercent} className="h-1" />
|
||||
</div>
|
||||
</div>
|
||||
{!hasPaidPlan ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-8 shrink-0 px-3 text-xs sm:self-center"
|
||||
onClick={() => router.push("/settings")}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
) : null}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ import {
|
|||
ExternalLink,
|
||||
MenuIcon,
|
||||
MessageCircleIcon,
|
||||
MoreHorizontal,
|
||||
LifeBuoy,
|
||||
LayoutGrid,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -24,6 +27,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
|
||||
import { useProject } from "@/stores"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
|
|
@ -126,7 +130,6 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="self-stretch w-px bg-[#FFFFFF33] hidden md:block" />
|
||||
{!isMobile && (
|
||||
<SpaceSelector
|
||||
selectedProjects={selectedProjects}
|
||||
|
|
@ -140,12 +143,12 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
<div className="flex items-center gap-2 z-10!">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void setViewMode("list")}
|
||||
onClick={() => void setViewMode("dashboard")}
|
||||
aria-label="Home"
|
||||
aria-current={viewMode === "list" ? "page" : undefined}
|
||||
aria-current={viewMode === "dashboard" ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex size-11 shrink-0 items-center justify-center rounded-full border cursor-pointer transition-colors",
|
||||
viewMode === "list"
|
||||
viewMode === "dashboard"
|
||||
? "border-[#2261CA33] bg-[#00173C] text-white"
|
||||
: "border-[#161F2C] bg-muted text-muted-foreground hover:bg-white/5",
|
||||
dmSansClassName(),
|
||||
|
|
@ -160,6 +163,11 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
mode: "list" as const,
|
||||
label: "Memories",
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{ mode: "graph" as const, label: "Graph", icon: GraphIcon },
|
||||
{
|
||||
mode: "integrations" as const,
|
||||
|
|
@ -231,6 +239,34 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
<Plus className="h-4 w-4 text-[#737373]" />
|
||||
Add memory
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void setViewMode("dashboard")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Home className="h-4 w-4 text-[#737373]" />
|
||||
Dashboard
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void setViewMode("list")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4 text-[#737373]" />
|
||||
Memories
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void setViewMode("graph")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<GraphIcon className="h-4 w-4 text-[#737373]" />
|
||||
Graph
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpenSearch?.()}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<SearchIcon className="h-4 w-4 text-[#737373]" />
|
||||
<span className="sr-only">Search</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setViewMode("integrations")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
|
|
@ -250,7 +286,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
onClick={handleFeedback}
|
||||
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]" />
|
||||
<LifeBuoy className="h-4 w-4 text-[#737373]" />
|
||||
Feedback
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
|
|
@ -265,56 +301,116 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onAddMemory}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
Add memory
|
||||
</div>
|
||||
<span
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="headers"
|
||||
className={cn(
|
||||
"rounded-full text-base h-10! shrink-0",
|
||||
"max-lg:w-10 max-lg:min-w-10 max-lg:justify-center max-lg:gap-0 max-lg:px-0",
|
||||
"lg:gap-2 lg:px-4 lg:w-auto lg:min-w-0",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={onAddMemory}
|
||||
aria-label="Add memory"
|
||||
>
|
||||
<Plus className="size-4 shrink-0" />
|
||||
<span className="max-lg:sr-only">Add memory</span>
|
||||
<span
|
||||
className={cn(
|
||||
"max-lg:hidden bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
C
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={dmSansClassName()}>
|
||||
Add memory (C)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="headers"
|
||||
className={cn(
|
||||
"rounded-full text-base h-10! shrink-0",
|
||||
"max-lg:w-10 max-lg:min-w-10 max-lg:justify-center max-lg:gap-0 max-lg:px-0",
|
||||
"lg:gap-2 lg:px-4 lg:w-auto lg:min-w-0",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={onOpenSearch}
|
||||
aria-label="Search"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0" />
|
||||
<span className="max-lg:hidden bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg
|
||||
className="size-[7.5px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Command Key</title>
|
||||
<path
|
||||
d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={dmSansClassName()}>
|
||||
Search (⌘K)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="headers"
|
||||
className={cn(
|
||||
"rounded-full h-10! w-10 min-w-10 justify-center px-0 lg:hidden",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
aria-label="More actions"
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
"min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
C
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onOpenSearch}
|
||||
>
|
||||
<SearchIcon className="size-4" />
|
||||
<span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg
|
||||
className="size-[7.5px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<DropdownMenuItem
|
||||
onClick={handleFeedback}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<title>Command Key</title>
|
||||
<path
|
||||
d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
<LifeBuoy className="h-4 w-4 text-[#737373]" />
|
||||
Feedback
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
className={cn(
|
||||
"hidden lg:flex rounded-full text-base gap-2 h-10!",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={handleFeedback}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircleIcon className="size-4" />
|
||||
<LifeBuoy className="size-4" />
|
||||
Feedback
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ export function MemoryGraph({
|
|||
maxNodes={maxNodes}
|
||||
canvasRef={canvasRef}
|
||||
totalCount={totalCount}
|
||||
colors={{
|
||||
bg: "transparent",
|
||||
edgeDerives: "#9ca3af",
|
||||
} as any}
|
||||
colors={
|
||||
{
|
||||
bg: "transparent",
|
||||
edgeDerives: "#9ca3af",
|
||||
} as any
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cn } from "@lib/utils"
|
|||
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
|
||||
import { DEFAULT_PROJECT_ID } from "@lib/constants"
|
||||
import {
|
||||
ChevronsLeftRight,
|
||||
ChevronDown,
|
||||
Plus,
|
||||
Trash2,
|
||||
XIcon,
|
||||
|
|
@ -57,8 +57,13 @@ export interface SpaceSelectorProps {
|
|||
}
|
||||
|
||||
const triggerVariants = {
|
||||
default: "px-3 py-2 rounded-md hover:bg-white/5",
|
||||
insideOut: "px-3 py-2 rounded-full bg-[#0D121A] shadow-inside-out",
|
||||
default:
|
||||
"h-10 min-h-10 shrink-0 rounded-full border border-[#161F2C] bg-muted px-3 gap-2 " +
|
||||
"hover:bg-white/5 " +
|
||||
"data-[state=open]:border-[#2261CA33] data-[state=open]:bg-[#00173C]/35 " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2261CA33]/35",
|
||||
insideOut:
|
||||
"h-10 min-h-10 gap-2 px-3 rounded-full bg-[#0D121A] shadow-inside-out hover:bg-[#121820]",
|
||||
}
|
||||
|
||||
export function SpaceSelector({
|
||||
|
|
@ -235,29 +240,71 @@ export function SpaceSelector({
|
|||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isLoading
|
||||
? "Loading spaces"
|
||||
: `Space: ${displayInfo.name}. Open menu to switch.`
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-pointer transition-colors focus:outline-none focus-visible:outline-none",
|
||||
"flex min-w-0 max-w-full items-center cursor-pointer transition-colors",
|
||||
triggerVariants[variant],
|
||||
variant === "default" &&
|
||||
compact &&
|
||||
"h-9 min-h-9 gap-1.5 px-2.5",
|
||||
dmSansClassName(),
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
{isNovaSpaces ? (
|
||||
<Globe className="size-4 text-white" />
|
||||
<Globe
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
variant === "insideOut" ? "text-white" : "text-[#737373]",
|
||||
compact ? "size-3.5" : "size-4",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
) : displayInfo.isMultiple ? (
|
||||
<Layers className="size-4 text-white" />
|
||||
<Layers
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
variant === "insideOut" ? "text-white" : "text-[#737373]",
|
||||
compact ? "size-3.5" : "size-4",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm font-bold tracking-[-0.98px]">
|
||||
<span
|
||||
className="shrink-0 text-sm font-bold tracking-[-0.98px]"
|
||||
aria-hidden
|
||||
>
|
||||
{displayInfo.emoji}
|
||||
</span>
|
||||
)}
|
||||
{!compact && (
|
||||
<span className="text-sm font-medium text-white">
|
||||
{isLoading ? "..." : displayInfo.name}
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate text-sm font-medium text-white",
|
||||
"max-w-[10rem] md:max-w-[15rem]",
|
||||
)}
|
||||
>
|
||||
{isLoading ? "…" : displayInfo.name}
|
||||
</span>
|
||||
)}
|
||||
{compact && (
|
||||
<span className="sr-only">
|
||||
{isLoading ? "Loading" : displayInfo.name}
|
||||
</span>
|
||||
)}
|
||||
{showChevron && (
|
||||
<ChevronsLeftRight className="size-4 rotate-90 text-white/70" />
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"shrink-0 opacity-90",
|
||||
variant === "insideOut" ? "text-white/80" : "text-[#737373]",
|
||||
compact ? "size-3.5" : "size-4",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import {
|
||||
|
|
@ -15,6 +16,7 @@ import { LogOut, Settings, RotateCcw, HelpCircle } from "lucide-react"
|
|||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
|
||||
import { useTokenUsage } from "@/hooks/use-token-usage"
|
||||
|
||||
export function UserProfileMenu({
|
||||
className,
|
||||
|
|
@ -26,6 +28,11 @@ export function UserProfileMenu({
|
|||
const { user } = useAuth()
|
||||
const router = useRouter()
|
||||
const { resetOrgOnboarded } = useOrgOnboarding()
|
||||
const autumn = useCustomer()
|
||||
const { currentPlan, isLoading: planLoading } = useTokenUsage(autumn)
|
||||
|
||||
const planBadgeLabel =
|
||||
currentPlan === "pro" ? "PRO" : currentPlan === "scale" ? "SCALE" : null
|
||||
|
||||
const handleTryOnboarding = () => {
|
||||
resetOrgOnboarded()
|
||||
|
|
@ -50,22 +57,38 @@ export function UserProfileMenu({
|
|||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
planBadgeLabel
|
||||
? `Account menu, ${planBadgeLabel} plan`
|
||||
: "Account menu"
|
||||
}
|
||||
className={cn(
|
||||
"rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-transform hover:scale-105",
|
||||
"relative inline-flex shrink-0 rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
className={cn(
|
||||
"border border-[#2E3033] h-8 w-8 md:h-10 md:w-10",
|
||||
"size-9 border border-[#161F2C]",
|
||||
avatarClassName,
|
||||
)}
|
||||
>
|
||||
<AvatarImage src={user.image ?? ""} />
|
||||
<AvatarFallback className="bg-[#0D121A] text-white">
|
||||
<AvatarFallback className="bg-[#0D121A] text-xs font-medium text-white">
|
||||
{user.name?.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{!planLoading && planBadgeLabel ? (
|
||||
<span
|
||||
id="user-plan-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute -bottom-0.5 left-1/2 z-10 -translate-x-1/2 rounded border px-1 py-px text-center text-[8px] font-bold uppercase leading-tight tracking-wide",
|
||||
"border-[#2261CA33] bg-[#00173C] text-[#6BB0FF]",
|
||||
)}
|
||||
>
|
||||
{planBadgeLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ export const analytics = {
|
|||
chatHistoryViewed: () => safeCapture("chat_history_viewed"),
|
||||
chatDeleted: () => safeCapture("chat_deleted"),
|
||||
|
||||
viewModeChanged: (mode: "graph" | "list" | "integrations" | "chat") =>
|
||||
safeCapture("view_mode_changed", { mode }),
|
||||
viewModeChanged: (
|
||||
mode: "dashboard" | "graph" | "list" | "integrations" | "chat",
|
||||
) => safeCapture("view_mode_changed", { mode }),
|
||||
|
||||
documentCardClicked: () => safeCapture("document_card_clicked"),
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,19 @@ export const shareParam = parseAsBoolean.withDefault(false)
|
|||
export const feedbackParam = parseAsBoolean.withDefault(false)
|
||||
|
||||
// View & filter states
|
||||
const viewLiterals = ["graph", "list", "integrations", "chat"] as const
|
||||
const viewLiterals = [
|
||||
"dashboard",
|
||||
"graph",
|
||||
"list",
|
||||
"integrations",
|
||||
"chat",
|
||||
] as const
|
||||
const integrationLiterals = ["import", "chrome", "connections"] as const
|
||||
export type IntegrationParamValue = (typeof integrationLiterals)[number]
|
||||
export const integrationParam = parseAsStringLiteral(integrationLiterals)
|
||||
export type ViewParamValue = (typeof viewLiterals)[number]
|
||||
export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("list")
|
||||
export const viewParam =
|
||||
parseAsStringLiteral(viewLiterals).withDefault("dashboard")
|
||||
|
||||
export const pluginsPanelParam = parseAsBoolean
|
||||
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
|
||||
|
|
|
|||
|
|
@ -197,7 +197,11 @@ export function MemoryGraph({
|
|||
setViewportVersion((v) => v + 1)
|
||||
}
|
||||
|
||||
const { hasMore: more, isLoadingMore: loading, onLoadMore: load } = loadMoreRef.current
|
||||
const {
|
||||
hasMore: more,
|
||||
isLoadingMore: loading,
|
||||
onLoadMore: load,
|
||||
} = loadMoreRef.current
|
||||
if (!more || loading || !load || !viewportRef.current) return
|
||||
|
||||
const vp = viewportRef.current
|
||||
|
|
@ -205,14 +209,17 @@ export function MemoryGraph({
|
|||
if (currentNodes.length === 0) return
|
||||
|
||||
const topLeft = vp.screenToWorld(0, 0)
|
||||
const bottomRight = vp.screenToWorld(containerSize.width, containerSize.height)
|
||||
const bottomRight = vp.screenToWorld(
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
)
|
||||
const viewW = bottomRight.x - topLeft.x
|
||||
const viewH = bottomRight.y - topLeft.y
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
let minX = Number.POSITIVE_INFINITY
|
||||
let minY = Number.POSITIVE_INFINITY
|
||||
let maxX = Number.NEGATIVE_INFINITY
|
||||
let maxY = Number.NEGATIVE_INFINITY
|
||||
for (const n of currentNodes) {
|
||||
if (n.x < minX) minX = n.x
|
||||
if (n.y < minY) minY = n.y
|
||||
|
|
@ -613,7 +620,6 @@ export function MemoryGraph({
|
|||
colors={colors}
|
||||
/>
|
||||
|
||||
|
||||
{!isLoading && !nodes.some((n) => n.type === "document") && children && (
|
||||
<div style={emptyStateStyle}>{children}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -119,10 +119,16 @@ export async function realClaudeMemoryExample() {
|
|||
const toolResults = []
|
||||
|
||||
if (responseData.content) {
|
||||
const memoryToolCalls = responseData.content.filter(
|
||||
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
|
||||
block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
const memoryToolCalls = responseData.content.filter(
|
||||
(
|
||||
block: any,
|
||||
): block is {
|
||||
type: "tool_use"
|
||||
id: string
|
||||
name: "memory"
|
||||
input: { command: MemoryCommand; path: string }
|
||||
} => block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
|
||||
const results = await Promise.all(
|
||||
memoryToolCalls.map((block: any) => {
|
||||
|
|
@ -196,10 +202,16 @@ export async function processClaudeResponse(
|
|||
const toolResults = []
|
||||
|
||||
if (claudeResponseData.content) {
|
||||
const memoryToolCalls = claudeResponseData.content.filter(
|
||||
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
|
||||
block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
const memoryToolCalls = claudeResponseData.content.filter(
|
||||
(
|
||||
block: any,
|
||||
): block is {
|
||||
type: "tool_use"
|
||||
id: string
|
||||
name: "memory"
|
||||
input: { command: MemoryCommand; path: string }
|
||||
} => block.type === "tool_use" && block.name === "memory",
|
||||
)
|
||||
|
||||
const results = await Promise.all(
|
||||
memoryToolCalls.map((block: any) =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue