mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-23 05:35:27 +00:00
feat(web): add workspace persona settings
This commit is contained in:
parent
566be20898
commit
7647888b4f
4 changed files with 105 additions and 129 deletions
|
|
@ -40,7 +40,7 @@ import { useEffect, useMemo, useRef, useState } from "react"
|
|||
import { toast } from "sonner"
|
||||
import { useContainerTags } from "@/hooks/use-container-tags"
|
||||
import { PopoverAnchor } from "@ui/components/popover"
|
||||
import { OrgContext } from "@/components/settings/org-context"
|
||||
import { WorkspacePersona } from "@/components/settings/workspace-persona"
|
||||
import { OrgPlanBadge } from "@/components/org-plan-badge"
|
||||
import { useTokenUsage } from "@/hooks/use-token-usage"
|
||||
import { useOrgSummaries } from "@/hooks/use-org-summaries"
|
||||
|
|
@ -614,7 +614,7 @@ export default function Account() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{canManageTeam && <OrgContext />}
|
||||
{canManageTeam && <WorkspacePersona />}
|
||||
|
||||
<DigestPreferences />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { LoaderIcon } from "lucide-react"
|
||||
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
|
||||
import { useOrgSettings, useUpdateOrgSettings } from "@/hooks/use-org-settings"
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-semibold text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function OrgContext() {
|
||||
const { data: settings, isLoading, isError } = useOrgSettings()
|
||||
const updateSettings = useUpdateOrgSettings()
|
||||
const [prompt, setPrompt] = useState("")
|
||||
|
||||
const enabled = settings?.shouldLLMFilter ?? false
|
||||
const savedPrompt = settings?.filterPrompt ?? ""
|
||||
const dirty = prompt.trim() !== savedPrompt.trim()
|
||||
const settingsReady = !isLoading && !isError
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(settings?.filterPrompt ?? "")
|
||||
}, [settings?.filterPrompt])
|
||||
|
||||
const handleToggle = (next: boolean) => {
|
||||
updateSettings.mutate({ shouldLLMFilter: next })
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings.mutate({
|
||||
shouldLLMFilter: true,
|
||||
filterPrompt: prompt.trim() ? prompt.trim() : null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="organization-context" className="flex flex-col gap-3 px-1">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<SectionTitle>Organization Context</SectionTitle>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] tracking-[-0.13px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Guide how Supermemory processes and remembers your content.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!settingsReady || updateSettings.isPending}
|
||||
onClick={() => handleToggle(!enabled)}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-full px-4 text-[12px] font-semibold transition-[color,opacity] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
enabled
|
||||
? "text-[#737373] hover:bg-white/[0.04] hover:text-[#A3A3A3]"
|
||||
: "bg-[#0D121A] text-[#FAFAFA] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] hover:opacity-80",
|
||||
)}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
)}
|
||||
{enabled ? "DISABLE" : "ENABLE"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className={cn(dmSansClassName(), "flex flex-col gap-2")}>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
placeholder="Tell Nova what to focus on, extract, and skip when turning your content into memories..."
|
||||
maxLength={750}
|
||||
className="min-h-[96px] w-full resize-y rounded-[12px] border border-white/[0.08] bg-[#0D121A] px-3.5 py-3 text-[13px] leading-relaxed text-[#FAFAFA] placeholder:text-[#525966] focus:border-white/[0.16] focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-[#737373] tabular-nums">
|
||||
{prompt.length}/750
|
||||
</span>
|
||||
{dirty && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPrompt(savedPrompt)}
|
||||
disabled={updateSettings.isPending}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"h-7 rounded-full px-3 text-[12px] font-medium text-[#737373] transition-colors hover:text-[#A3A3A3] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={updateSettings.isPending}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"inline-flex h-7 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-semibold text-[#FAFAFA] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
101
apps/web/components/settings/workspace-persona.tsx
Normal file
101
apps/web/components/settings/workspace-persona.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"use client"
|
||||
|
||||
import { LoaderIcon } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useOrgSettings, useUpdateOrgSettings } from "@/hooks/use-org-settings"
|
||||
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-semibold text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkspacePersona() {
|
||||
const { data: settings, isLoading, isError } = useOrgSettings()
|
||||
const updateSettings = useUpdateOrgSettings()
|
||||
const [persona, setPersona] = useState("")
|
||||
|
||||
const savedPersona = settings?.workspacePersona ?? ""
|
||||
const dirty = persona.trim() !== savedPersona.trim()
|
||||
const settingsReady = !isLoading && !isError
|
||||
|
||||
useEffect(() => {
|
||||
setPersona(settings?.workspacePersona ?? "")
|
||||
}, [settings?.workspacePersona])
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings.mutate({
|
||||
workspacePersona: persona.trim() ? persona.trim() : null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="workspace-persona" className="flex flex-col gap-3 px-1">
|
||||
<div className="min-w-0">
|
||||
<SectionTitle>Workspace Persona</SectionTitle>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] tracking-[-0.13px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Guide how Company Brain communicates and works with your workspace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={cn(dmSansClassName(), "flex flex-col gap-2")}>
|
||||
<textarea
|
||||
value={persona}
|
||||
onChange={(event) => setPersona(event.target.value)}
|
||||
placeholder="Describe your workspace's goals, communication style, and how you prefer Company Brain to collaborate with your team..."
|
||||
maxLength={1500}
|
||||
disabled={!settingsReady || updateSettings.isPending}
|
||||
className="min-h-[96px] w-full resize-y rounded-[12px] border border-white/[0.08] bg-[#0D121A] px-3.5 py-3 text-[13px] leading-relaxed text-[#FAFAFA] placeholder:text-[#525966] focus:border-white/[0.16] focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-[#737373] tabular-nums">
|
||||
{persona.length}/1500
|
||||
</span>
|
||||
{dirty && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPersona(savedPersona)}
|
||||
disabled={updateSettings.isPending}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"h-7 rounded-full px-3 text-[12px] font-medium text-[#737373] transition-colors hover:text-[#A3A3A3] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={updateSettings.isPending}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"inline-flex h-7 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-semibold text-[#FAFAFA] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ const API_BASE = `${process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermem
|
|||
export type OrgSettings = {
|
||||
shouldLLMFilter: boolean
|
||||
filterPrompt: string | null
|
||||
workspacePersona: string | null
|
||||
includeItems?: string[] | null
|
||||
excludeItems?: string[] | null
|
||||
}
|
||||
|
|
@ -34,6 +35,7 @@ export function useOrgSettings() {
|
|||
return {
|
||||
shouldLLMFilter: settings.shouldLLMFilter ?? false,
|
||||
filterPrompt: settings.filterPrompt ?? null,
|
||||
workspacePersona: settings.workspacePersona ?? null,
|
||||
includeItems: settings.includeItems ?? null,
|
||||
excludeItems: settings.excludeItems ?? null,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue