supermemory/apps/web/hooks/use-brain-skills.impl.ts
sreedharsreeram f14cdd7a4c feat(web): add Company Brain skills settings (#1322)
## Stack Context

This is the frontend half of the Company Brain Skills feature. The harness is implemented in supermemoryai/mono#2611.

## What?

Add Skills settings with separate Org-wide and Personal sections, Markdown upload autofill, scoped creation and editing, approval controls, and server-driven permissions.

## Why?

Members need a focused way to manage their private playbooks while admins create and approve organization-wide guidance.

## Related

- Harness: https://github.com/supermemoryai/mono/pull/2611

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/d8c77451-8f55-47ab-a182-5c98da616263)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-08-02 05:36:32 +00:00

182 lines
4.3 KiB
TypeScript

"use client"
import { useAuth } from "@lib/auth-context"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import type {
NewSkillOrigin,
SkillDraft,
SkillScope,
} from "@/components/settings/company-brain-skills/domain"
import { skillSaveRequestBody } from "@/components/settings/company-brain-skills/domain"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const BASE = `${BACKEND}/brain/skills`
export type BrainSkillStatus = "active" | "disabled"
export type BrainSkill = {
id: string
name: string
description: string
body: string
scope: SkillScope
status: BrainSkillStatus
creatorUserId: string
canEdit: boolean
canDelete: boolean
version: number
createdAt: number
updatedAt: number
rejectionReason: string | null
}
export type BrainSkillsResponse = {
skills: BrainSkill[]
isAdmin: boolean
viewerId: string
}
export const brainSkillKeys = {
all: (orgId: string | undefined, userId: string | undefined) =>
["brain", "skills", orgId, userId] as const,
list: (orgId: string | undefined, userId: string | undefined) =>
[...brainSkillKeys.all(orgId, userId), "list"] as const,
}
class BrainSkillRequestError extends Error {
readonly status: number
constructor(message: string, status: number) {
super(message)
this.name = "BrainSkillRequestError"
this.status = status
}
}
async function responseError(response: Response, fallback: string) {
const body = (await response.json().catch(() => null)) as {
error?: string | { message?: string }
message?: string
} | null
const nested =
typeof body?.error === "object" ? body.error.message : undefined
const message =
body?.message ??
(typeof body?.error === "string" ? body.error : undefined) ??
nested ??
fallback
return new BrainSkillRequestError(message, response.status)
}
async function jsonRequest<T>(
url: string,
init: RequestInit,
fallback: string,
) {
const response = await fetch(url, { credentials: "include", ...init })
if (!response.ok) throw await responseError(response, fallback)
return (await response.json()) as T
}
export function useBrainSkills() {
const { org, user } = useAuth()
return useQuery({
queryKey: brainSkillKeys.list(org?.id, user?.id),
queryFn: () =>
jsonRequest<BrainSkillsResponse>(`${BASE}/`, {}, "Couldn't load skills."),
enabled: !!org?.id && !!user?.id,
})
}
function useInvalidateBrainSkills() {
const { org, user } = useAuth()
const queryClient = useQueryClient()
return () =>
queryClient.invalidateQueries({
queryKey: brainSkillKeys.all(org?.id, user?.id),
})
}
export function useSaveBrainSkill() {
const invalidate = useInvalidateBrainSkills()
return useMutation({
mutationFn: ({
id,
draft,
createOrigin,
expectedVersion,
}: {
id: string | null
draft: SkillDraft
createOrigin?: NewSkillOrigin
expectedVersion?: number
}) =>
jsonRequest<{ skill: BrainSkill }>(
id ? `${BASE}/${id}` : `${BASE}/`,
{
method: id ? "PUT" : "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(
skillSaveRequestBody(draft, id, createOrigin, expectedVersion),
),
},
"Couldn't save this skill.",
),
onSuccess: () => {
void invalidate()
},
onError: (error) => {
if (error instanceof BrainSkillRequestError && error.status === 409) {
void invalidate()
}
},
})
}
export function useDeleteBrainSkill() {
const invalidate = useInvalidateBrainSkills()
return useMutation({
mutationFn: ({
id,
expectedVersion,
}: {
id: string
expectedVersion: number
}) =>
jsonRequest<{ ok?: boolean }>(
`${BASE}/${id}`,
{
method: "DELETE",
headers: { "content-type": "application/json" },
body: JSON.stringify({ expectedVersion }),
},
"Couldn't delete this skill.",
),
onSuccess: () => {
void invalidate()
},
onError: (error) => {
if (error instanceof BrainSkillRequestError && error.status === 409) {
void invalidate()
}
},
})
}
export function useUploadBrainSkill() {
return useMutation({
mutationFn: (content: string) =>
jsonRequest<{
draft: Partial<SkillDraft> & { origin?: "upload" }
}>(
`${BASE}/upload`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content }),
},
"Couldn't read this skill file.",
),
})
}