"use client" import { cn } from "@lib/utils" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@ui/components/alert-dialog" import { Building2, Loader2, Trash2, UserRound } from "lucide-react" import { useEffect, useState } from "react" import ReactMarkdown from "react-markdown" import { toast } from "sonner" import { type BrainSkill, useDeleteBrainSkill, useSaveBrainSkill, } from "@/hooks/use-brain-skills" import { dmSans125ClassName } from "@/lib/fonts" import { canSelectSkillScope, SKILL_BODY_MAX_LENGTH, SKILL_DESCRIPTION_MAX_LENGTH, SKILL_NAME_MAX_LENGTH, setSkillDraftScope, skillDraftPayload, skillDraftForRole, skillDraftsEqual, type NewSkillOrigin, type SkillDraft, type SkillScope, } from "./domain" const inputClass = cn( dmSans125ClassName(), "w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] px-3 text-[13px] text-[#FAFAFA] outline-none transition-colors focus:border-white/[0.16] read-only:cursor-default read-only:text-[#B5BDC9] disabled:opacity-50", ) const labelClass = cn( dmSans125ClassName(), "text-[11px] font-medium uppercase tracking-[0.06em] text-[#687282]", ) const secondaryButtonClass = cn( dmSans125ClassName(), "inline-flex h-9 items-center justify-center gap-2 rounded-full border border-white/10 px-4 text-[13px] font-medium text-[#9AA3B2] transition-colors hover:bg-white/[0.04] hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-45", ) const primaryButtonClass = cn( dmSans125ClassName(), "inline-flex h-9 items-center justify-center gap-2 rounded-full bg-[#14161A] px-4 text-[13px] font-semibold text-[#FAFAFA] shadow-inside-out transition-colors hover:bg-[#121820] disabled:cursor-not-allowed disabled:opacity-45", ) function messageFor(error: unknown): string | null { return error instanceof Error ? error.message : error ? "Something went wrong." : null } function ScopeButton({ value, label, icon, selected, disabled, onSelect, }: { value: SkillScope label: string icon: React.ReactNode selected: boolean disabled: boolean onSelect: (scope: SkillScope) => void }) { return ( ) } function MarkdownPreview({ body }: { body: string }) { return (
{body.trim() ? ( {body} ) : (

Nothing to preview yet.

)}
) } export function SkillEditor({ skill, initialDraft, isAdmin, viewerId, draftKind, createOrigin, onClose, onDirtyChange, }: { skill: BrainSkill | null initialDraft: SkillDraft isAdmin: boolean viewerId: string draftKind?: "new" | "upload" createOrigin?: NewSkillOrigin onClose: () => void onDirtyChange?: (dirty: boolean) => void }) { const privateSkillUnavailable = !!skill && skill.scope === "personal" && skill.creatorUserId !== viewerId const [draft, setDraft] = useState(() => privateSkillUnavailable ? { name: "", description: "", body: "", scope: "personal" as const } : skill ? initialDraft : skillDraftForRole(initialDraft, isAdmin), ) const [preview, setPreview] = useState(false) const [clientError, setClientError] = useState(null) const [deleteOpen, setDeleteOpen] = useState(false) const save = useSaveBrainSkill() const remove = useDeleteBrainSkill() const isDirty = !privateSkillUnavailable && !skillDraftsEqual(draft, initialDraft) useEffect(() => { onDirtyChange?.(isDirty) }, [isDirty, onDirtyChange]) if (privateSkillUnavailable) { return (

Personal skill unavailable

This private skill is only available to its creator.

) } const isCreator = !skill || skill.creatorUserId === viewerId const canManage = skill ? skill.canEdit : isAdmin || draft.scope === "personal" const canDelete = skill?.canDelete ?? false const canSelectScope = (scope: SkillScope) => canSelectSkillScope(skill, viewerId, isAdmin, scope) const showSharedScopeOptions = isAdmin const busy = save.isPending || remove.isPending const bodyBytes = new TextEncoder().encode(draft.body).length const mutationError = messageFor(save.error) ?? messageFor(remove.error) const visibleError = clientError ?? mutationError const scopePermissionCopy = !skill && !isAdmin ? "New member skills are personal. Only admins and owners can create org-wide skills." : skill?.scope === "personal" && isCreator && !isAdmin ? "This skill stays personal. Only creators who are also admins or owners can make personal skills org-wide." : skill?.scope !== "personal" && isAdmin && !isCreator ? "Only the original creator can convert this org-wide skill to Personal." : null const resetErrors = () => { setClientError(null) save.reset() remove.reset() } const set = (key: K, value: SkillDraft[K]) => { resetErrors() setDraft((current) => ({ ...current, [key]: value })) } const saveDraft = () => { if (busy || !canManage) return let payload: SkillDraft try { payload = skillDraftPayload(draft) } catch (error) { setClientError(messageFor(error)) return } save.mutate( { id: skill?.id ?? null, draft: payload, createOrigin, expectedVersion: skill?.version, }, { onSuccess: () => { toast.success("Skill saved.") onClose() }, }, ) } return (

{draftKind === "upload" ? "Review uploaded skill" : skill ? canManage ? "Edit skill" : "View skill" : "New skill"}

{draftKind === "upload" ? (

Not saved yet

) : null}
{!canManage ? (

{skill?.scope === "personal" ? "Only this skill’s creator can edit or delete it. Admin access does not override personal ownership. You can still read the full playbook below." : isAdmin ? "You can view this org-wide skill, but it cannot be edited or deleted in its current state." : "Only admins and owners can edit or delete org-wide skills. You can still read the full playbook below."}

) : null} {skill?.status === "disabled" ? (

This skill is disabled. {skill.rejectionReason ? ` Reason: ${skill.rejectionReason}` : null}

) : null} {canManage ? (
Who can use it
} selected={draft.scope === "personal"} disabled={!canSelectScope("personal") || busy} onSelect={(scope) => { resetErrors() setDraft((current) => setSkillDraftScope(current, scope)) }} /> {showSharedScopeOptions ? ( } selected={draft.scope === "org"} disabled={!canSelectScope("org") || busy} onSelect={(scope) => { resetErrors() setDraft((current) => setSkillDraftScope(current, scope)) }} /> ) : null}
{scopePermissionCopy ? (

{scopePermissionCopy}

) : null}
) : null}