mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-22 05:26:17 +00:00
fix(web): align workspace prompt settings contract
This commit is contained in:
parent
78017586e8
commit
562e721539
3 changed files with 39 additions and 61 deletions
|
|
@ -1,11 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { LoaderIcon } from "lucide-react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { useOrgSettings, useUpdateOrgSettings } from "@/hooks/use-org-settings"
|
||||
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
|
||||
import { cn } from "@lib/utils"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
|
||||
const DESCRIPTION_ID = "workspace-prompt-description"
|
||||
const COUNTER_ID = "workspace-prompt-counter"
|
||||
|
|
@ -36,65 +35,54 @@ function PromptHeader() {
|
|||
"text-[13px] tracking-[-0.13px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Set persistent guidance for operating preferences, priorities, source
|
||||
and tool choices, workflows, terminology, formatting, and communication
|
||||
style. This guidance cannot override fixed safety, access, or approval
|
||||
constraints.
|
||||
Set persistent guidance for how Company Brain works across your
|
||||
workspace.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkspacePrompt() {
|
||||
const { org } = useAuth()
|
||||
const settingsQuery = useOrgSettings()
|
||||
const updateSettings = useUpdateOrgSettings()
|
||||
const [prompt, setPrompt] = useState("")
|
||||
const syncedPrompt = useRef<{ orgId: string; value: string } | null>(null)
|
||||
const [draft, setDraft] = useState<string | null>(null)
|
||||
|
||||
const savedPrompt = settingsQuery.data?.workspacePrompt ?? ""
|
||||
const dirty = prompt.trim() !== savedPrompt.trim()
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsQuery.data) return
|
||||
|
||||
const nextPrompt = settingsQuery.data.workspacePrompt ?? ""
|
||||
const orgId = org?.id ?? ""
|
||||
const previous = syncedPrompt.current
|
||||
|
||||
setPrompt((current) => {
|
||||
if (!previous || previous.orgId !== orgId) return nextPrompt
|
||||
return current === previous.value ? nextPrompt : current
|
||||
})
|
||||
syncedPrompt.current = { orgId, value: nextPrompt }
|
||||
}, [org?.id, settingsQuery.data])
|
||||
const prompt = draft ?? savedPrompt
|
||||
const dirty = draft !== null && draft.trim() !== savedPrompt.trim()
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings.mutate({
|
||||
workspacePrompt: prompt.trim() ? prompt.trim() : null,
|
||||
})
|
||||
updateSettings.mutate(
|
||||
{
|
||||
workspacePrompt: prompt.trim() ? prompt.trim() : null,
|
||||
},
|
||||
{ onSuccess: () => setDraft(null) },
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="workspace-prompt"
|
||||
aria-labelledby={HEADING_ID}
|
||||
aria-busy={settingsQuery.isLoading || updateSettings.isPending}
|
||||
className="flex flex-col gap-3 px-1"
|
||||
>
|
||||
<PromptHeader />
|
||||
|
||||
{settingsQuery.isLoading ? (
|
||||
<div
|
||||
<output
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"flex min-h-[96px] items-center justify-center gap-2 rounded-[12px] border border-white/[0.08] bg-[#0D121A] text-[12px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
<LoaderIcon aria-hidden="true" className="size-3 animate-spin" />
|
||||
Loading workspace prompt…
|
||||
</div>
|
||||
</output>
|
||||
) : settingsQuery.isError ? (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(dmSansClassName(), "flex flex-col items-start gap-2")}
|
||||
>
|
||||
<p className="text-[13px] text-[#A3A3A3]">
|
||||
|
|
@ -104,10 +92,10 @@ export function WorkspacePrompt() {
|
|||
type="button"
|
||||
onClick={() => void settingsQuery.refetch()}
|
||||
disabled={settingsQuery.isFetching}
|
||||
className="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"
|
||||
className="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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1B1F24] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{settingsQuery.isFetching && (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
<LoaderIcon aria-hidden="true" className="size-3 animate-spin" />
|
||||
)}
|
||||
Try again
|
||||
</button>
|
||||
|
|
@ -118,10 +106,10 @@ export function WorkspacePrompt() {
|
|||
aria-labelledby={HEADING_ID}
|
||||
aria-describedby={`${DESCRIPTION_ID} ${COUNTER_ID}`}
|
||||
value={prompt}
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
placeholder="Add persistent guidance for how Company Brain should operate across your workspace..."
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
placeholder="Describe operating preferences, priorities, source and tool choices, workflows, terminology, formatting, and communication style. Fixed safety, access, and approval constraints still apply."
|
||||
maxLength={1500}
|
||||
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"
|
||||
className="min-h-[96px] w-full resize-none rounded-[12px] border border-white/[0.08] bg-[#0D121A] px-3.5 py-3 text-[13px] leading-relaxed text-[#FAFAFA] placeholder:text-[#525966] focus-visible:border-white/[0.16] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1B1F24]"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<span
|
||||
|
|
@ -134,11 +122,11 @@ export function WorkspacePrompt() {
|
|||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPrompt(savedPrompt)}
|
||||
onClick={() => setDraft(null)}
|
||||
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",
|
||||
"h-7 rounded-full px-3 text-[12px] font-medium text-[#737373] transition-colors hover:text-[#A3A3A3] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1B1F24] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
|
|
@ -149,11 +137,14 @@ export function WorkspacePrompt() {
|
|||
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",
|
||||
"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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1B1F24] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
<LoaderIcon
|
||||
aria-hidden="true"
|
||||
className="size-3 animate-spin"
|
||||
/>
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ export type OrgSettings = {
|
|||
excludeItems?: string[] | null
|
||||
}
|
||||
|
||||
type OrgSettingsResponse = {
|
||||
settings?: Partial<OrgSettings>
|
||||
} & Partial<OrgSettings>
|
||||
|
||||
export function useOrgSettings() {
|
||||
const { org } = useAuth()
|
||||
const orgId = org?.id ?? ""
|
||||
|
|
@ -22,14 +18,11 @@ export function useOrgSettings() {
|
|||
return useQuery({
|
||||
queryKey: ["settings", "org", orgId],
|
||||
queryFn: async (): Promise<OrgSettings> => {
|
||||
const response = await $fetch("@get/settings", {
|
||||
disableValidation: true,
|
||||
})
|
||||
const response = await $fetch("@get/settings")
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message || "Failed to load settings")
|
||||
}
|
||||
const data = response.data as OrgSettingsResponse | null
|
||||
const settings = data?.settings ?? data ?? {}
|
||||
const settings = response.data ?? {}
|
||||
return {
|
||||
shouldLLMFilter: settings.shouldLLMFilter ?? false,
|
||||
filterPrompt: settings.filterPrompt ?? null,
|
||||
|
|
@ -63,7 +56,7 @@ export function useUpdateOrgSettings() {
|
|||
onMutate: () => ({ orgId }),
|
||||
onSuccess: async (data, _settings, mutationContext) => {
|
||||
const queryKey = ["settings", "org", mutationContext.orgId] as const
|
||||
const canonicalSettings = data?.settings
|
||||
const canonicalSettings = data?.updated
|
||||
if (canonicalSettings) {
|
||||
queryClient.setQueryData<OrgSettings>(queryKey, (current) =>
|
||||
current ? { ...current, ...canonicalSettings } : current,
|
||||
|
|
|
|||
|
|
@ -29,16 +29,10 @@ import {
|
|||
UpdateContainerTagSettingsRequestSchema,
|
||||
} from "../validation/api"
|
||||
|
||||
// Settings response schema - this is custom to console (not in shared validation)
|
||||
const SettingsResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
settings: z.object({
|
||||
excludeItems: z.array(z.string().min(1).max(20)).nullable().optional(),
|
||||
filterPrompt: z.string().max(750).nullable().optional(),
|
||||
includeItems: z.array(z.string().min(1).max(20)).nullable().optional(),
|
||||
shouldLLMFilter: z.boolean().optional(),
|
||||
workspacePrompt: z.string().max(1500).nullable().optional(),
|
||||
}),
|
||||
const UpdateSettingsResponseSchema = z.object({
|
||||
orgId: z.string(),
|
||||
orgSlug: z.string(),
|
||||
updated: SettingsRequestSchema,
|
||||
})
|
||||
|
||||
// Analytics request schema - custom to console
|
||||
|
|
@ -196,11 +190,11 @@ export const apiSchema = createSchema({
|
|||
|
||||
// Settings operations
|
||||
"@get/settings": {
|
||||
output: z.object({}).passthrough(),
|
||||
output: SettingsRequestSchema,
|
||||
},
|
||||
"@patch/settings": {
|
||||
input: SettingsRequestSchema,
|
||||
output: SettingsResponseSchema,
|
||||
output: UpdateSettingsResponseSchema,
|
||||
},
|
||||
"@post/settings/reset": {
|
||||
input: z.object({ confirmation: z.string() }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue