mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-22 03:01:07 +00:00
fix responsiveness for graph layout (#910)
This commit is contained in:
parent
debac33eba
commit
47bd9805ef
5 changed files with 160 additions and 48 deletions
|
|
@ -515,12 +515,11 @@ export default function NewPage() {
|
|||
const gradientTopPosition = gradientTopPositionForWidth(viewportWidth)
|
||||
|
||||
const isChatView = viewMode === "chat"
|
||||
const isGraphMode = viewMode === "graph" && !isMobile
|
||||
const isGraphMode = viewMode === "graph"
|
||||
const isMemoriesDesktop = viewMode === "list" && !isMobile
|
||||
const isHomeDesktop = viewMode === "dashboard" && !isMobile
|
||||
const showNovaBackdrop = isGraphMode || isMemoriesDesktop || isHomeDesktop
|
||||
const isDashboardShell =
|
||||
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
|
||||
const isDashboardShell = viewMode === "dashboard"
|
||||
|
||||
return (
|
||||
<HotkeysProvider>
|
||||
|
|
@ -629,7 +628,7 @@ export default function NewPage() {
|
|||
<XBookmarksDetailView
|
||||
onBack={() => void setViewMode("integrations")}
|
||||
/>
|
||||
) : viewMode === "graph" && !isMobile ? (
|
||||
) : viewMode === "graph" ? (
|
||||
<div className="min-h-0 min-w-0 flex-1">
|
||||
<GraphLayoutView />
|
||||
</div>
|
||||
|
|
@ -674,20 +673,7 @@ export default function NewPage() {
|
|||
) : (
|
||||
<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
|
||||
}
|
||||
headerNotice={undefined}
|
||||
highlights={highlightsData?.highlights ?? []}
|
||||
isLoadingHighlights={isLoadingHighlights}
|
||||
onAddMemory={handleAddMemory}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useQueryState } from "nuqs"
|
|||
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { FileTextIcon, GlobeIcon, ZapIcon, Loader2 } from "lucide-react"
|
||||
import { FileTextIcon, GlobeIcon, ZapIcon, Loader2, XIcon } from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { ConnectContent } from "./connections"
|
||||
import { NoteContent } from "./note"
|
||||
|
|
@ -35,10 +35,10 @@ export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
|
|||
<Dialog open={isOpen} onOpenChange={(open: boolean) => !open && onClose()}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"border-none bg-[#1B1F24] flex flex-col p-3 md:p-4 gap-3",
|
||||
"border-none bg-[#1B1F24] flex flex-col",
|
||||
isMobile
|
||||
? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
|
||||
: "w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! rounded-[22px]",
|
||||
? "top-2! left-2! translate-x-0! translate-y-0! w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-[18px] p-0 gap-0 overflow-hidden"
|
||||
: "w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! rounded-[22px] p-4 gap-3",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -48,7 +48,7 @@ export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
|
|||
showCloseButton={false}
|
||||
>
|
||||
<DialogTitle className="sr-only">Add Document</DialogTitle>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<AddDocument onClose={onClose} isOpen={isOpen} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
@ -256,19 +256,44 @@ export function AddDocument({
|
|||
activeTab === "file" && (!fileTabHasPending || isSubmitting)
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col md:flex-row text-white md:space-x-5 space-y-3 md:space-y-0">
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden text-white md:flex-row md:space-x-5">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-between",
|
||||
isMobile ? "w-full" : "w-1/3",
|
||||
isMobile
|
||||
? "w-full shrink-0 border-b border-[#0F1621] bg-[#1B1F24] px-3 pt-3 pb-3"
|
||||
: "w-1/3",
|
||||
)}
|
||||
>
|
||||
{isMobile && (
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium text-white",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Add memory
|
||||
</p>
|
||||
<p className="text-xs text-[#737373]">
|
||||
Save something to recall later
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="flex size-9 items-center justify-center rounded-full border border-[#1F2937] bg-[#0D121A] text-[#8B8B8B] transition-colors hover:text-white disabled:opacity-50"
|
||||
aria-label="Close add memory"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-1",
|
||||
isMobile
|
||||
? "flex-row overflow-x-auto pb-2 scrollbar-thin"
|
||||
: "flex-col",
|
||||
isMobile ? "grid grid-cols-4 gap-1" : "flex flex-col gap-1",
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
|
|
@ -285,6 +310,31 @@ export function AddDocument({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<UsageMeter
|
||||
label="Credits"
|
||||
value={
|
||||
isLoadingUsage
|
||||
? "..."
|
||||
: `${tokensToCredits(tokensUsed)} / ${tokensToCredits(tokensLimit)}`
|
||||
}
|
||||
percent={tokensPercent}
|
||||
active={hasPaidPlan}
|
||||
/>
|
||||
<UsageMeter
|
||||
label="Searches"
|
||||
value={
|
||||
isLoadingUsage
|
||||
? "..."
|
||||
: `${formatUsageNumber(searchesUsed)} / ${formatUsageNumber(searchesLimit)}`
|
||||
}
|
||||
percent={searchesPercent}
|
||||
active={hasPaidPlan}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div data-testid="usage-counter" className="flex flex-col gap-3 mr-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -385,11 +435,11 @@ export function AddDocument({
|
|||
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col flex-1 min-h-0 px-1",
|
||||
isMobile ? "w-full" : "w-2/3",
|
||||
"flex min-h-0 flex-1 flex-col",
|
||||
isMobile ? "w-full px-3 pt-3" : "w-2/3 px-1",
|
||||
)}
|
||||
>
|
||||
<div className="overflow-auto flex-1 min-h-0 scrollbar-thin">
|
||||
<div className="min-h-0 flex-1 overflow-auto scrollbar-thin">
|
||||
{activeTab === "note" && (
|
||||
<NoteContent
|
||||
onSubmit={handleNoteSubmit}
|
||||
|
|
@ -423,8 +473,10 @@ export function AddDocument({
|
|||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 pt-3 shrink-0",
|
||||
isMobile ? "flex-col" : "justify-between",
|
||||
"flex shrink-0 gap-2",
|
||||
isMobile
|
||||
? "mx-[-0.75rem] mt-3 border-t border-[#0F1621] bg-[#1B1F24] px-3 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
||||
: "justify-between pt-3",
|
||||
)}
|
||||
>
|
||||
{!isMobile && (
|
||||
|
|
@ -438,13 +490,19 @@ export function AddDocument({
|
|||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("flex items-center gap-2", isMobile && "justify-end")}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
isMobile && "w-full justify-end",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="text-[#737373] cursor-pointer rounded-full"
|
||||
className={cn(
|
||||
"cursor-pointer rounded-full text-[#737373]",
|
||||
isMobile && "h-11 px-4",
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -455,6 +513,7 @@ export function AddDocument({
|
|||
disabled={
|
||||
activeTab === "file" ? fileTabSubmitDisabled : isSubmitting
|
||||
}
|
||||
className={cn(isMobile && "h-11 min-w-[8rem] px-5")}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
|
@ -485,6 +544,53 @@ export function AddDocument({
|
|||
)
|
||||
}
|
||||
|
||||
function UsageMeter({
|
||||
label,
|
||||
value,
|
||||
percent,
|
||||
active,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
percent: number
|
||||
active: boolean
|
||||
}) {
|
||||
const safePercent = Math.max(0, Math.min(100, percent))
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-[#0F1621] bg-[#10151C] px-3 py-2 shadow-inside-out">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<span className="truncate text-[11px] font-medium text-[#FAFAFA]">
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 text-[10px] font-medium",
|
||||
active ? "text-[#4BA0FA]" : "text-[#8B8B8B]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[#2E353D] p-px">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${safePercent}%`,
|
||||
background:
|
||||
safePercent > 80
|
||||
? "#ef4444"
|
||||
: active
|
||||
? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)"
|
||||
: "#0054AD",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
active,
|
||||
onClick,
|
||||
|
|
@ -508,19 +614,24 @@ function TabButton({
|
|||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-full text-left transition-colors whitespace-nowrap focus:outline-none focus:ring-0 shrink-0",
|
||||
active ? "bg-[#14161A] shadow-inside-out" : "hover:bg-[#14161A]/50",
|
||||
"relative flex h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-xl px-1 text-center transition-colors focus:outline-none focus:ring-0",
|
||||
active
|
||||
? "bg-[#0F141B] text-white shadow-inside-out ring-1 ring-[#2261CA33]"
|
||||
: "text-[#8B8B8B] hover:bg-[#14161A]/50",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("size-4 shrink-0 text-white")} />
|
||||
<Icon className="size-3.5 shrink-0" />
|
||||
<span
|
||||
className={cn("font-medium text-white text-sm", dmSansClassName())}
|
||||
className={cn(
|
||||
"min-w-0 truncate text-xs font-medium leading-none",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{title.split(" ")[0]}
|
||||
</span>
|
||||
{isPro && (
|
||||
<span className="bg-[#4BA0FA] text-black text-[8px] font-semibold px-1 py-0.5 rounded">
|
||||
<span className="absolute top-1 right-1 rounded bg-[#4BA0FA] px-1 py-0.5 text-[7px] font-semibold leading-none text-black">
|
||||
PRO
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,12 @@ export function NoteContent({
|
|||
}, [isOpen, onContentChange])
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-y-auto flex-1 w-full h-full mb-4! bg-[#14161A] shadow-inside-out rounded-[14px]">
|
||||
<div className="flex h-full min-h-[45dvh] w-full flex-1 overflow-y-auto rounded-[14px] bg-[#10151C] p-3 shadow-inside-out ring-1 ring-[#202A36] md:mb-4! md:bg-[#14161A] md:p-4 md:ring-0">
|
||||
<TextEditor
|
||||
content={undefined}
|
||||
onContentChange={handleContentChange}
|
||||
onSubmit={handleSubmit}
|
||||
debounceMs={0}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { memo, useCallback, useRef } from "react"
|
||||
import { useQueryState } from "nuqs"
|
||||
import Image from "next/image"
|
||||
import { Share2 } from "lucide-react"
|
||||
import { MemoryGraph } from "./memory-graph"
|
||||
import { useProject } from "@/stores"
|
||||
import { useGraphHighlights } from "@/stores/highlights"
|
||||
|
|
@ -30,7 +31,7 @@ export const GraphLayoutView = memo(function GraphLayoutView() {
|
|||
}, [setIsShareModalOpen])
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-0 w-full">
|
||||
<div className="relative h-full min-h-[calc(100dvh-8.5rem)] w-full md:min-h-0">
|
||||
{/* Full-width graph */}
|
||||
<div className="absolute inset-0">
|
||||
<MemoryGraph
|
||||
|
|
@ -44,22 +45,26 @@ export const GraphLayoutView = memo(function GraphLayoutView() {
|
|||
</div>
|
||||
|
||||
{/* Share graph button - top left */}
|
||||
<div className="absolute top-4 left-4 z-15">
|
||||
<div className="absolute left-3 top-3 z-15 md:left-4 md:top-4">
|
||||
<Button
|
||||
variant="headers"
|
||||
className={cn(
|
||||
"rounded-full text-base gap-2 h-10!",
|
||||
"size-10 rounded-full p-0 md:size-auto md:h-10! md:px-4",
|
||||
"md:gap-2 md:text-base",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={handleShare}
|
||||
aria-label="Share graph"
|
||||
>
|
||||
<Image
|
||||
src="/icons/share-graph.svg"
|
||||
alt="Share"
|
||||
width={16}
|
||||
height={16}
|
||||
className="hidden md:block"
|
||||
/>
|
||||
Share graph
|
||||
<Share2 className="size-4 md:hidden" />
|
||||
<span className="hidden md:inline">Share graph</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ export function TextEditor({
|
|||
content: initialContent,
|
||||
onContentChange,
|
||||
onSubmit,
|
||||
debounceMs = 500,
|
||||
}: {
|
||||
content: string | undefined
|
||||
onContentChange: (content: string) => void
|
||||
onSubmit: () => void
|
||||
debounceMs?: number
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const editorRef = useRef<Editor | null>(null)
|
||||
|
|
@ -36,7 +38,7 @@ export function TextEditor({
|
|||
const json = editor.getJSON()
|
||||
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
|
||||
onContentChange?.(markdown)
|
||||
}, 500)
|
||||
}, debounceMs)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions,
|
||||
|
|
@ -48,6 +50,13 @@ export function TextEditor({
|
|||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
editorRef.current = editor
|
||||
if (!hasUserEditedRef.current) return
|
||||
if (debounceMs === 0) {
|
||||
const json = editor.getJSON()
|
||||
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
|
||||
onContentChange?.(markdown)
|
||||
return
|
||||
}
|
||||
debouncedUpdates(editor)
|
||||
},
|
||||
editorProps: {
|
||||
|
|
@ -120,7 +129,7 @@ export function TextEditor({
|
|||
tabIndex={0}
|
||||
ref={containerRef}
|
||||
onClick={handleClick}
|
||||
className="w-full h-full outline-none prose prose-invert max-w-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:focus:outline-none [&_.ProseMirror-focused]:outline-none text-editor-prose cursor-text"
|
||||
className="h-full w-full cursor-text outline-none prose prose-invert max-w-none text-editor-prose [&_.ProseMirror]:min-h-full [&_.ProseMirror]:outline-none [&_.ProseMirror]:text-[15px] [&_.ProseMirror]:leading-6 [&_.ProseMirror]:text-[#D7DEE8] [&_.ProseMirror-focused]:outline-none [&_.ProseMirror]:focus:outline-none"
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue