finalise org context and profile.md at space level

This commit is contained in:
ved015 2026-05-26 15:18:01 +05:30
parent 9bd79cdb80
commit 2f61a708a1
4 changed files with 150 additions and 96 deletions

View file

@ -55,10 +55,17 @@ import {
CheckIcon,
LayoutGrid,
Loader,
MoreHorizontal,
Trash2Icon,
UserRound,
XIcon,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { useProcessingDocuments } from "@/hooks/use-processing-documents"
import { TimelineView } from "./timeline-view"
import { SpaceProfilePanel } from "@/components/space-profile-panel"
@ -633,32 +640,53 @@ export function MemoriesGrid({
<AlignLeft className="size-3.5" />
Timeline
</button>
<button
type="button"
aria-pressed={profileOpen}
className={cn(
"inline-flex h-full items-center justify-center gap-1.5 rounded-full border px-2.5 text-xs font-medium cursor-pointer transition-colors",
profileOpen
? "border-[#2261CA33] bg-[#00173C] text-white"
: "border-transparent text-[#737373] hover:bg-white/5",
)}
onClick={handleToggleProfile}
>
<UserRound className="size-3.5" />
Profile
</button>
</div>
{onEnterSelectionMode && (
<button
type="button"
aria-label="Select documents"
title="Select documents"
className="size-8 flex items-center justify-center rounded-full border border-[#161F2C] bg-[#0D121A] hover:bg-[#00173C] hover:border-[#2261CA33] transition-colors cursor-pointer"
onClick={onEnterSelectionMode}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="More memory actions"
className={cn(
dmSansClassName(),
"inline-flex h-8 items-center justify-center gap-1.5 rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 text-xs font-medium transition-colors cursor-pointer",
profileOpen
? "border-[#2261CA33] bg-[#00173C] text-white"
: "text-[#737373] hover:bg-white/5",
)}
>
<MoreHorizontal className="size-3.5" />
More
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={cn(
"min-w-[180px] rounded-xl border border-[#2E3033] p-1.5 shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
dmSansClassName(),
)}
style={{
background:
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<BoxSelect className="size-4 text-[#737373]" />
</button>
)}
{onEnterSelectionMode && (
<DropdownMenuItem
onSelect={onEnterSelectionMode}
className="gap-2 rounded-md px-3 py-2.5 text-sm font-medium text-white cursor-pointer hover:bg-[#293952]/40"
>
<BoxSelect className="size-4 text-[#737373]" />
Select memories
</DropdownMenuItem>
)}
<DropdownMenuItem
onSelect={handleToggleProfile}
className="gap-2 rounded-md px-3 py-2.5 text-sm font-medium text-white cursor-pointer hover:bg-[#293952]/40"
>
<UserRound className="size-4 text-[#737373]" />
Space profile
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}
@ -837,7 +865,11 @@ export function MemoriesGrid({
</div>
<AnimatePresence initial={false}>
{profileOpen && !isMobile && (
<SpaceProfilePanel containerTag={selectedProject} isOpen />
<SpaceProfilePanel
containerTag={selectedProject}
isOpen
onClose={() => setProfileOpen(false)}
/>
)}
</AnimatePresence>
</div>

View file

@ -15,6 +15,9 @@ type ContextTemplate = {
prompt: string
}
const SURFACE_SHADOW =
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset"
const CONTEXT_TEMPLATES: ContextTemplate[] = [
{
id: "personal-general",
@ -100,12 +103,14 @@ function PillButton({
disabled={disabled}
className={cn(
dmSansClassName(),
"inline-flex h-9 items-center justify-center gap-2 rounded-[9px] border px-3 text-[13px] font-medium transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
"inline-flex h-9 items-center justify-center gap-2 rounded-full border px-4 text-[13px] font-semibold transition-opacity cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
"bg-[#0D121A] text-[#FAFAFA] hover:opacity-80",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
variant === "primary"
? "border-transparent bg-[#0054AD] text-[#FAFAFA] hover:bg-[#0B65C9]"
? "border-transparent"
: variant === "danger"
? "border-red-500/20 bg-red-500/10 text-red-300 hover:bg-red-500/15"
: "border-[#161F2C] bg-[#0D121A] text-[#FAFAFA] hover:bg-[#00173C] hover:border-[#2261CA33]",
? "border-transparent"
: "border-transparent",
)}
>
{children}
@ -224,7 +229,7 @@ export function OrgContext() {
</div>
{enabled && !isManaging && savedPrompt && (
<div className="rounded-[12px] border border-[#161F2C] bg-[#0D121A] p-4">
<div className="rounded-[12px] border border-white/[0.08] bg-[#0D121A] p-4">
<p className="line-clamp-6 whitespace-pre-wrap text-[13px] leading-relaxed text-[#A3A3A3]">
{savedPrompt}
</p>
@ -245,54 +250,56 @@ export function OrgContext() {
)}
{enabled && isManaging && (
<div className="flex flex-col gap-4">
<label className="flex flex-col gap-2">
<span className="text-[13px] font-medium text-[#FAFAFA]">
What should Nova focus on?
</span>
<textarea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder="Describe what Nova should extract, skip, and prioritize when turning your content into memories..."
className={cn(
dmSansClassName(),
"min-h-[180px] w-full resize-y rounded-[12px] border border-[#161F2C] bg-[#0D121A] p-4 text-[13px] leading-relaxed text-[#FAFAFA] placeholder:text-[#525966] focus:outline-none focus:ring-1 focus:ring-[#2261CA66]",
)}
maxLength={750}
/>
<span className="self-end text-[11px] text-[#737373]">
{prompt.length}/750
</span>
</label>
<div className="overflow-hidden rounded-[18px] border border-white/[0.08] bg-[#1B1F24]">
<div className="flex flex-col gap-4 p-4">
<label className="flex flex-col gap-2">
<span className="text-[13px] font-medium text-[#FAFAFA]">
What should Nova focus on?
</span>
<textarea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder="Describe what Nova should extract, skip, and prioritize when turning your content into memories..."
className={cn(
dmSansClassName(),
"min-h-[180px] w-full resize-y rounded-[14px] border border-white/[0.08] bg-[#0D121A] p-4 text-[13px] leading-relaxed text-[#FAFAFA] placeholder:text-[#525966] focus:border-white/[0.14] focus:outline-none",
)}
maxLength={750}
/>
<span className="self-end text-[11px] text-[#737373]">
{prompt.length}/750
</span>
</label>
<div className="grid gap-3 md:grid-cols-2">
{CONTEXT_TEMPLATES.map((template) => {
const isSelected = selectedTemplateId === template.id
return (
<button
key={template.id}
type="button"
onClick={() => setPrompt(template.prompt)}
className={cn(
"rounded-[10px] border p-3 text-left transition-colors cursor-pointer",
"bg-[#0D121A] hover:bg-[#121A24]",
isSelected
? "border-[#1C2B3E] bg-[#1C2B3E]"
: "border-white/10",
)}
>
<p className="text-[14px] font-medium text-[#FAFAFA]">
{template.label}
</p>
<p className="mt-1 text-[12px] leading-snug text-[#737373]">
{template.description}
</p>
</button>
)
})}
<div className="grid gap-3 md:grid-cols-2">
{CONTEXT_TEMPLATES.map((template) => {
const isSelected = selectedTemplateId === template.id
return (
<button
key={template.id}
type="button"
onClick={() => setPrompt(template.prompt)}
className={cn(
"rounded-[14px] border p-4 text-left transition-colors cursor-pointer",
"bg-[#14161A] hover:bg-[#121820]",
isSelected
? "border-white/[0.16] bg-[#171B22]"
: "border-white/[0.08]",
)}
>
<p className="text-[14px] font-medium text-[#FAFAFA]">
{template.label}
</p>
<p className="mt-1 text-[12px] leading-snug text-[#737373]">
{template.description}
</p>
</button>
)
})}
</div>
</div>
<div className="flex justify-end gap-2">
<div className="flex justify-end gap-2 border-t border-white/[0.08] bg-[#171B22] px-4 py-3">
<PillButton onClick={handleCancel}>CANCEL</PillButton>
<PillButton
onClick={handleSave}
@ -318,8 +325,11 @@ export function OrgContext() {
>
<DialogContent
showCloseButton={false}
style={{
boxShadow: SURFACE_SHADOW,
}}
className={cn(
"sm:max-w-[440px] border-none bg-[#1B1F24] p-0 gap-0 rounded-[22px] overflow-hidden",
"sm:max-w-[440px] border border-white/[0.12] bg-[#1B1F24] p-0 gap-0 rounded-[22px] overflow-hidden",
dmSansClassName(),
)}
>
@ -347,13 +357,7 @@ export function OrgContext() {
: "Nova will stop using this guidance for new content, and the saved context will be cleared."}
</p>
</div>
<DialogPrimitive.Close
className="flex size-7 shrink-0 items-center justify-center rounded-full border border-[rgba(115,115,115,0.2)] bg-[#0D121A] text-[#737373] transition-opacity hover:opacity-100 focus:outline-hidden cursor-pointer"
style={{
boxShadow:
"inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<DialogPrimitive.Close className="flex size-7 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#737373] shadow-inside-out transition-opacity hover:opacity-80 focus:outline-hidden cursor-pointer">
<X className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>

View file

@ -19,15 +19,19 @@ export function SpaceProfileModal({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
className={cn(
"w-[94%]! max-w-[440px]! max-h-[82dvh] border-none bg-[#14161A] p-4 rounded-[22px]",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
"w-[94%]! max-w-[440px]! max-h-[82dvh] border border-white/[0.08] bg-[#1B1F24] p-4 rounded-[22px]",
"shadow-[0_2.842px_14.211px_rgba(0,0,0,0.25),inset_0.711px_0.711px_0.711px_rgba(255,255,255,0.10)]",
dmSansClassName(),
)}
>
<DialogTitle className="sr-only">Space Profile</DialogTitle>
<div className="flex min-h-[420px] flex-col">
<SpaceProfileContent containerTag={containerTag} />
<SpaceProfileContent
containerTag={containerTag}
onClose={() => onOpenChange(false)}
/>
</div>
</DialogContent>
</Dialog>

View file

@ -1,6 +1,6 @@
"use client"
import { Brain } from "lucide-react"
import { Brain, X } from "lucide-react"
import { motion } from "motion/react"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { useSpaceProfile } from "@/hooks/use-space-profile"
@ -9,15 +9,17 @@ import { cn } from "@lib/utils"
type SpaceProfilePanelProps = {
containerTag: string
isOpen: boolean
onClose: () => void
}
type SpaceProfileContentProps = {
containerTag: string
onClose?: () => void
}
function CountBadge({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex h-[18px] min-w-[22px] items-center justify-center rounded-[4px] bg-[#1C2B3E] px-1.5 text-[10px] font-semibold text-[#A3A3A3]">
<span className="inline-flex h-[18px] min-w-[22px] items-center justify-center rounded-[4px] bg-white/[0.05] px-1.5 text-[10px] font-semibold text-[#A3A3A3]">
{children}
</span>
)
@ -38,9 +40,9 @@ function LoadingState() {
function EmptyState() {
return (
<div className="flex min-h-[180px] flex-col items-center justify-center rounded-[14px] border border-[#161F2C] bg-[#0D121A] px-5 py-8 text-center">
<div className="mb-3 flex size-10 items-center justify-center rounded-full bg-[#00173C]">
<Brain className="size-4 text-[#4BA0FA]" />
<div className="flex min-h-[180px] flex-col items-center justify-center rounded-[14px] border border-white/[0.08] bg-[#14161A] px-5 py-8 text-center">
<div className="mb-3 flex size-10 items-center justify-center rounded-full bg-[#0D121A] shadow-inside-out">
<Brain className="size-4 text-[#A3A3A3]" />
</div>
<p className="text-[14px] font-medium text-[#FAFAFA]">No profile yet</p>
<p className="mt-1 text-[12px] leading-relaxed text-[#737373]">
@ -87,6 +89,7 @@ function ProfileSection({ label, items }: { label: string; items: string[] }) {
export function SpaceProfileContent({
containerTag,
onClose,
}: SpaceProfileContentProps) {
const { data, isLoading, error } = useSpaceProfile(containerTag)
const keyFacts = data?.static ?? []
@ -95,7 +98,7 @@ export function SpaceProfileContent({
return (
<div className={cn(dmSansClassName(), "flex min-h-0 flex-1 flex-col")}>
<div className="border-b border-[#161F2C] pb-3">
<div className="flex items-start justify-between gap-3 border-b border-white/[0.08] pb-3">
<div>
<h3
className={cn(
@ -109,13 +112,23 @@ export function SpaceProfileContent({
What Nova knows in this space
</p>
</div>
{onClose ? (
<button
type="button"
aria-label="Close space profile"
onClick={onClose}
className="flex size-7 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#737373] shadow-inside-out transition-opacity hover:opacity-80 cursor-pointer"
>
<X className="size-4" />
</button>
) : null}
</div>
<div className="min-h-0 flex-1 overflow-y-auto pt-4 scrollbar-thin">
{isLoading ? (
<LoadingState />
) : error ? (
<p className="rounded-[12px] border border-red-500/20 bg-red-500/10 p-3 text-[13px] text-red-300">
<p className="rounded-[12px] border border-white/[0.08] bg-[#14161A] p-3 text-[13px] text-[#A3A3A3]">
Failed to load space profile.
</p>
) : totalCount === 0 ? (
@ -134,6 +147,7 @@ export function SpaceProfileContent({
export function SpaceProfilePanel({
containerTag,
isOpen,
onClose,
}: SpaceProfilePanelProps) {
if (!isOpen) return null
@ -144,11 +158,11 @@ export function SpaceProfilePanel({
exit={{ opacity: 0, width: 0, x: 12 }}
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
className={cn(
"hidden h-full w-[300px] shrink-0 flex-col rounded-[14px] bg-[#14161A] p-4 md:flex",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
"hidden h-full w-[300px] shrink-0 flex-col rounded-[18px] border border-white/[0.08] bg-[#1B1F24] p-4 md:flex",
"shadow-[0_2.842px_14.211px_rgba(0,0,0,0.25),inset_0.711px_0.711px_0.711px_rgba(255,255,255,0.10)]",
)}
>
<SpaceProfileContent containerTag={containerTag} />
<SpaceProfileContent containerTag={containerTag} onClose={onClose} />
</motion.aside>
)
}